Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/MIGRATION_FROM_CLAUDE_CODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ API key. It is no longer chasing 1:1 parity — see

## TL;DR — the 5-minute switch

DeepCode reads your existing Claude Code assets **in place** — you do not have to
move anything to try it:

| Read in place | Notes |
| ----------------------------- | --------------------------------------------------- |
| `~/.claude/settings.json` | Used when `~/.deepcode/settings.json` doesn't exist |
| `~/.claude/CLAUDE.md` | Loaded as user memory |
| `CLAUDE.md` (any project dir) | Loaded alongside `DEEPCODE.md` / `AGENTS.md` |
| `~/.claude/skills/` | A same-named DeepCode skill wins |
| `~/.claude/agents/` | A same-named DeepCode agent wins |

A project's `.claude/settings.json` is **not** read: project settings pass
through the directory-trust gate, and that boundary is not widened implicitly.

The copy below is only needed if you want DeepCode to own its own copies.

```bash
# 1. Install DeepCode CLI
npm install -g deepcode-cli
Expand Down
137 changes: 137 additions & 0 deletions packages/core/src/config/claude-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Reading a Claude Code user's existing assets in place.
//
// The migration guide asked people to `mv ~/.claude/... ~/.deepcode/...` before
// DeepCode would see anything they had. That is five steps of grit in front of
// `npm i -g deepcode-cli && deepcode`, and it is the first thing a migrating
// user hits.

import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { beforeEach, describe, expect, it } from 'vitest';
import { loadSettings, resolveUserSettingsPath } from './loader.js';
import { loadMemory } from '../memory/loader.js';
import { loadSkills } from '../skills/loader.js';
import { loadSubAgents } from '../sub-agents/loader.js';

let home: string;
let cwd: string;

beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'dc-claude-compat-'));
cwd = join(home, 'project');
await mkdir(cwd, { recursive: true });
});

const writeAt = async (path: string, body: string): Promise<void> => {
await mkdir(join(path, '..'), { recursive: true });
await writeFile(path, body, 'utf8');
};

describe('settings', () => {
it('reads ~/.claude/settings.json when DeepCode has none', async () => {
await writeAt(join(home, '.claude', 'settings.json'), JSON.stringify({ model: 'from-claude' }));
expect(await resolveUserSettingsPath({ cwd, home })).toBe(join(home, '.claude/settings.json'));
const loaded = await loadSettings({ cwd, home });
expect(loaded.merged.model).toBe('from-claude');
});

it('prefers DeepCode settings outright when both exist', async () => {
await writeAt(join(home, '.claude', 'settings.json'), JSON.stringify({ model: 'from-claude' }));
await writeAt(
join(home, '.deepcode', 'settings.json'),
JSON.stringify({ effortLevel: 'high' }),
);
const loaded = await loadSettings({ cwd, home });
expect(loaded.merged.effortLevel).toBe('high');
// A fallback, not an extra layer — no silent merge from the other file.
expect(loaded.merged.model).toBeUndefined();
expect(loaded.sources.userPath).toBe(join(home, '.deepcode/settings.json'));
});

it('names the file it actually read, so provenance stays honest', async () => {
await writeAt(join(home, '.claude', 'settings.json'), JSON.stringify({ model: 'from-claude' }));
const loaded = await loadSettings({ cwd, home });
expect(loaded.sources.userPath).toContain('.claude');
expect(Object.values(loaded.provenance)).toContainEqual(
expect.objectContaining({ path: join(home, '.claude/settings.json') }),
);
});

it('does not fall back when an explicit data directory is given', async () => {
await writeAt(join(home, '.claude', 'settings.json'), JSON.stringify({ model: 'from-claude' }));
const directory = join(home, 'explicit');
expect(await resolveUserSettingsPath({ cwd, home, directory })).toBe(
join(directory, 'settings.json'),
);
});

it('does not read a project .claude/settings.json — that path is trust-gated', async () => {
await writeAt(join(cwd, '.claude', 'settings.json'), JSON.stringify({ model: 'untrusted' }));
const loaded = await loadSettings({ cwd, home });
expect(loaded.merged.model).toBeUndefined();
});
});

describe('memory', () => {
it('reads ~/.claude/CLAUDE.md', async () => {
await writeAt(join(home, '.claude', 'CLAUDE.md'), 'global claude instructions');
const memory = await loadMemory({ cwd, home });
expect(memory.text).toContain('global claude instructions');
});

it('reads a project CLAUDE.md', async () => {
await writeAt(join(cwd, 'CLAUDE.md'), 'project claude instructions');
const memory = await loadMemory({ cwd, home });
expect(memory.text).toContain('project claude instructions');
});

it('places DEEPCODE.md after CLAUDE.md at the same level, so it wins', async () => {
await writeAt(join(cwd, 'CLAUDE.md'), 'claude says');
await writeAt(join(cwd, 'DEEPCODE.md'), 'deepcode says');
const memory = await loadMemory({ cwd, home });
expect(memory.text.indexOf('claude says')).toBeLessThan(memory.text.indexOf('deepcode says'));
});
});

describe('skills and agents', () => {
it('loads skills from ~/.claude/skills', async () => {
await writeAt(
join(home, '.claude', 'skills', 'greet', 'SKILL.md'),
'---\nname: greet\ndescription: say hi\n---\nbody',
);
const skills = await loadSkills({ cwd, home });
expect(skills.map((s) => s.qualifiedName)).toContain('greet');
});

it('lets a DeepCode skill shadow a same-named Claude Code one', async () => {
await writeAt(
join(home, '.claude', 'skills', 'greet', 'SKILL.md'),
'---\nname: greet\ndescription: from claude\n---\nbody',
);
await writeAt(
join(home, '.deepcode', 'skills', 'greet', 'SKILL.md'),
'---\nname: greet\ndescription: from deepcode\n---\nbody',
);
const skills = (await loadSkills({ cwd, home })).filter((s) => s.qualifiedName === 'greet');
expect(skills).toHaveLength(1);
expect(skills[0]!.frontmatter.description).toBe('from deepcode');
});

it('loads sub-agents from ~/.claude/agents, with DeepCode winning collisions', async () => {
await writeAt(
join(home, '.claude', 'agents', 'scout.md'),
'---\nname: scout\ndescription: from claude\n---\nbody',
);
const fromClaude = await loadSubAgents({ cwd, home });
expect(fromClaude.map((a) => a.qualifiedName)).toContain('scout');

await writeAt(
join(home, '.deepcode', 'agents', 'scout.md'),
'---\nname: scout\ndescription: from deepcode\n---\nbody',
);
const both = (await loadSubAgents({ cwd, home })).filter((a) => a.qualifiedName === 'scout');
expect(both).toHaveLength(1);
expect(both[0]!.frontmatter.description).toBe('from deepcode');
});
});
32 changes: 31 additions & 1 deletion packages/core/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,36 @@ export function settingsPaths(opts: LoadSettingsOpts): LoadedSettings['sources']
};
}

/**
* Where the user layer comes from when DeepCode has no settings of its own.
*
* A Claude Code user's `~/.claude/settings.json` is read in place rather than
* requiring `mv ~/.claude/settings.json ~/.deepcode/settings.json`. It is a
* *fallback*, not an extra layer: the moment `~/.deepcode/settings.json`
* exists it wins outright, so provenance keeps naming one real file and the
* trust gate keeps seeing exactly the layers it already knows about.
*
* User-level only. A project's `.claude/settings.json` is not read: project
* settings pass through the directory-trust gate, and quietly widening what
* that gate covers is not a change to make in passing.
*/
export async function resolveUserSettingsPath(opts: LoadSettingsOpts): Promise<string> {
const preferred = settingsPaths(opts).userPath;
if (opts.directory) return preferred; // explicit data dir — no fallback
try {
await fs.access(preferred);
return preferred;
} catch {
const claudePath = join(opts.home ?? homedir(), '.claude', 'settings.json');
try {
await fs.access(claudePath);
return claudePath;
} catch {
return preferred;
}
}
}

async function readJson(path: string): Promise<DeepCodeSettings | undefined> {
try {
const raw = await fs.readFile(path, 'utf8');
Expand All @@ -79,7 +109,7 @@ async function readJsonRequired(path: string): Promise<DeepCodeSettings> {
}

export async function loadSettings(opts: LoadSettingsOpts): Promise<LoadedSettings> {
const sources = settingsPaths(opts);
const sources = { ...settingsPaths(opts), userPath: await resolveUserSettingsPath(opts) };
const [user, project, local, override] = await Promise.all([
readJson(sources.userPath),
readJson(sources.projectPath),
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/memory/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export async function loadMemory(opts: LoadMemoryOpts): Promise<LoadedMemory> {
await addRaw(abs, label, raw, depth);
};

// 0. ~/.claude/CLAUDE.md — a Claude Code user's existing global instructions.
// Read first so DeepCode's own files can override it, and read in place
// rather than requiring `mv ~/.claude ~/.deepcode`: the migration guide's
// five-step copy was the largest piece of grit in the way of trying this.
await addFile(join(home, '.claude', 'CLAUDE.md'), 'CLAUDE.md (Claude Code)', 0);

// 1. ~/.deepcode/DEEPCODE.md (user-level)
await addFile(join(directory, 'DEEPCODE.md'), 'user memory', 0);

Expand All @@ -123,10 +129,13 @@ export async function loadMemory(opts: LoadMemoryOpts): Promise<LoadedMemory> {
0,
);

// 2. DEEPCODE.md walking from cwd → root, deepest first
const upwards = walkUpwards(opts.cwd, home);
// 2. CLAUDE.md / DEEPCODE.md walking from cwd → root, deepest first.
// Reverse so root-most first, deepest last (later overrides via concat — Claude Code semantics)
const upwards = walkUpwards(opts.cwd, home);
for (const dir of upwards.reverse()) {
// CLAUDE.md before DEEPCODE.md at each level: same-directory DeepCode
// instructions win over the Claude Code ones they were derived from.
await addFile(join(dir, 'CLAUDE.md'), `${dir}/CLAUDE.md`, 0);
await addFile(join(dir, 'DEEPCODE.md'), `${dir}/DEEPCODE.md`, 0);
}

Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/skills/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ export async function loadSkills(opts: LoadSkillsOpts): Promise<Skill[]> {
await loadFromDir(opts.builtinDir, 'builtin', out);
}

// 2. User-level
// 2. User-level — DeepCode's own first, so it wins a name collision, then a
// Claude Code user's ~/.claude/skills read in place rather than moved.
await loadFromDir(join(directory, 'skills'), 'user', out);
if (!opts.directory) {
await loadFromDir(join(home, '.claude', 'skills'), 'user', out);
}

// 3. Project-level
await loadFromDir(join(opts.cwd, '.deepcode', 'skills'), 'project', out);
Expand Down Expand Up @@ -116,6 +120,10 @@ async function loadFromDir(
}
if (front.disabled === true) continue;
const qualifiedName = pluginName ? `${pluginName}:${front.name}` : front.name;
// First definition of a name wins. Callers load in precedence order, so a
// DeepCode skill shadows the Claude Code skill it was derived from instead
// of both being listed to the model.
if (out.some((skill) => skill.qualifiedName === qualifiedName)) continue;
out.push({
qualifiedName,
frontmatter: front as SkillFrontmatter,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/sub-agents/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export async function loadSubAgents(opts: LoadSubAgentsOpts): Promise<SubAgent[]
const home = opts.home ?? homedir();
const out: SubAgent[] = [];
await loadFromDir(join(home, '.deepcode', 'agents'), 'user', out);
// A Claude Code user's agents, read in place rather than moved.
await loadFromDir(join(home, '.claude', 'agents'), 'user', out);
await loadFromDir(
opts.projectDirOverride ?? join(opts.cwd, '.deepcode', 'agents'),
'project',
Expand Down Expand Up @@ -74,6 +76,10 @@ async function loadFromDir(
const front = fields as unknown as Partial<SubAgentFrontmatter>;
if (!front.name || !front.description) continue;
const qualifiedName = pluginName ? `${pluginName}:${front.name}` : front.name;
// First definition wins, matching findAgent()'s .find(). Callers load in
// precedence order, so a DeepCode agent shadows a same-named Claude Code
// one rather than the list carrying both.
if (out.some((agent) => agent.qualifiedName === qualifiedName)) continue;
out.push({
qualifiedName,
frontmatter: front as SubAgentFrontmatter,
Expand Down
Loading