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
55 changes: 52 additions & 3 deletions apps/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import {
contextWindowFor,
estimateCost,
redact,
writeSettings,
EFFORT_PARAMS,
VERSION,
type Credentials,
type Effort,
} from '@deepcode/core';
import { execFile } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -102,6 +104,18 @@ export function formatPrComments(data: PrCommentsData): string[] {
return lines;
}

/** Set a possibly-dotted key path on an object, creating intermediate objects. */
function setDeep(obj: Record<string, unknown>, path: string, value: unknown): void {
const keys = path.split('.');
let o = obj;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]!;
if (typeof o[k] !== 'object' || o[k] === null || Array.isArray(o[k])) o[k] = {};
o = o[k] as Record<string, unknown>;
}
o[keys[keys.length - 1]!] = value;
}

export interface SessionContext {
cwd: string;
model: string;
Expand All @@ -111,6 +125,8 @@ export interface SessionContext {
creds: Credentials;
/** Credentials store (REPL-injected) — backs /login and /logout. */
credsStore?: CredentialsStore;
/** User settings.json path (REPL-injected, honors --home) — backs /config set. */
userSettingsPath?: string;
sessionId: string;
sessions: SessionManager;
usage: {
Expand Down Expand Up @@ -345,12 +361,45 @@ export const ContextCommand: SlashCommand = {

export const ConfigCommand: SlashCommand = {
name: '/config',
description: 'Show resolved settings (read-only in M2).',
run(_args, ctx) {
description: 'Show settings, or `/config set <key> <value>` to edit (dotted keys ok).',
async run(args, ctx) {
if (args[0] === 'set') {
const key = args[1]?.trim();
const valueRaw = args.slice(2).join(' ').trim();
if (!key || !valueRaw) {
return [
'Usage: /config set <key> <value>',
' key may be dotted (e.g. permissions.defaultMode); value is parsed as JSON, else kept as a string.',
];
}
if (!ctx.userSettingsPath) return ['(/config set is unavailable here.)'];
let value: unknown;
try {
value = JSON.parse(valueRaw);
} catch {
value = valueRaw;
}
let current: Record<string, unknown> = {};
try {
current = JSON.parse(await readFile(ctx.userSettingsPath, 'utf8')) as Record<
string,
unknown
>;
} catch {
/* missing/empty → start fresh */
}
setDeep(current, key, value);
await writeSettings(ctx.userSettingsPath, current as DeepCodeSettings);
return [
`Set ${key} = ${JSON.stringify(value)}`,
`→ ${ctx.userSettingsPath}`,
'Applies to new sessions (model / mode / effort change live via /model, /mode, /effort).',
];
}
const out = ['Current settings (merged):'];
out.push(JSON.stringify(ctx.settings, null, 2).split('\n').slice(0, 40).join('\n'));
out.push('');
out.push('Edit ~/.deepcode/settings.json (user) or .deepcode/settings.json (project).');
out.push('Edit with `/config set <key> <value>`, or ~/.deepcode/settings.json directly.');
return out;
},
};
Expand Down
30 changes: 29 additions & 1 deletion apps/cli/src/parity-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// never real creds). /recap uses a mock provider; /pr_comments' renderer is pure.

import { afterEach, describe, expect, it } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { CredentialsStore, SessionManager } from '@deepcode/core';
Expand Down Expand Up @@ -154,3 +154,31 @@ describe('/upgrade + /privacy-settings', () => {
expect(out).toMatch(/security-model\.md/);
});
});

describe('/config set', () => {
it('writes a dotted key to the user settings file', async () => {
const path = join(await tmpHome(), 'settings.json');
const out = await reg
.match('/config')!
.cmd.run(['set', 'permissions.defaultMode', 'plan'], ctx({ userSettingsPath: path }));
expect(out.join('\n')).toMatch(/Set permissions\.defaultMode/);
const written = JSON.parse(await readFile(path, 'utf8')) as {
permissions?: { defaultMode?: string };
};
expect(written.permissions?.defaultMode).toBe('plan');
});

it('parses a JSON value (number, not string)', async () => {
const path = join(await tmpHome(), 'settings.json');
await reg
.match('/config')!
.cmd.run(['set', 'memoryLoadCapKB', '200'], ctx({ userSettingsPath: path }));
const written = JSON.parse(await readFile(path, 'utf8')) as { memoryLoadCapKB?: number };
expect(written.memoryLoadCapKB).toBe(200);
});

it('shows usage for `/config set` with no key/value', async () => {
const out = await reg.match('/config')!.cmd.run(['set'], ctx());
expect(out.join('\n')).toMatch(/Usage: \/config set/);
});
});
1 change: 1 addition & 0 deletions apps/cli/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
settings,
creds,
credsStore,
userSettingsPath: settingsPaths({ cwd, home: opts.home }).userPath,
sessionId: session.id,
sessions,
usage: { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0 },
Expand Down
2 changes: 1 addition & 1 deletion docs/BEHAVIOR_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Legend: `✅` matches · `🟡` matches with caveats · `🔄` deferred · `⚠
| `/effort` | ✓ | ✓ | 🟡 — CLI prints the tier table (numbers from `EFFORT_PARAMS` SSOT); switch via `/effort <tier>`; arrow-key selector is GUI-only (M6) |
| `/cost` / `/usage` | ✓ | ✓ | ✅ |
| `/context` | ✓ | ✓ | ✅ |
| `/config` | ✓ | ✓ (read-only) | 🟡 — Claude Code's `/config` is interactive editor; ours is JSON dump (M3c-ext for editor) |
| `/config` | ✓ | ✓ | 🟡 — dumps merged settings + `/config set <key> <value>` (dotted keys, JSON values) writes user settings; no full arrow-key editor |
| `/resume` | ✓ | ✓ (list only) | 🟡 — Claude Code has fuzzy picker; ours lists; pick via `--resume <id>` |
| `/init` | ✓ | ✓ | ✅ — interactive 3-phase REPL flow (scan → draft → approve-write `AGENTS.md`) |
| `/mcp` | ✓ | ✓ | ✅ |
Expand Down
Loading