cli-five 0.2.15 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ import kleur from 'kleur';
2
+ import { log } from '../util/log.mjs';
3
+ import { ADDON_NAMES, getAddon, listAddons } from '../addons/registry.mjs';
4
+
5
+ /**
6
+ * `cli-five add <name>` — dispatch to a registered add-on.
7
+ *
8
+ * This pass ships the dispatcher only. Registered targets without a `run`
9
+ * function respond with a clear stub so the mechanism can be exercised
10
+ * end-to-end without pretending an integration exists.
11
+ */
12
+ export async function add(args) {
13
+ const name = args._[1];
14
+
15
+ if (!name || name === '--help' || name === '-h' || name === 'help') {
16
+ printAddHelp();
17
+ return;
18
+ }
19
+
20
+ const addon = getAddon(name);
21
+ if (!addon) {
22
+ log.err(`Unknown add-on: ${name}`);
23
+ printAvailable(addon => addon.name);
24
+ process.exitCode = 2;
25
+ return;
26
+ }
27
+
28
+ if (typeof addon.run !== 'function') {
29
+ log.warn(`${addon.label} is registered but not implemented yet.`);
30
+ log.info('The add dispatcher works — this target is reserved for a future release.');
31
+ if (addon.note || addon.status) log.dim(addon.note || addon.status);
32
+ log.dim('Nothing was written to your project.');
33
+ return;
34
+ }
35
+
36
+ log.step(`add ${addon.name}`);
37
+ await addon.run({ cwd: args.cwd, args });
38
+ }
39
+
40
+ function printAddHelp() {
41
+ process.stdout.write(`${kleur.bold('cli-five add')} — install an optional add-on\n\n`);
42
+ process.stdout.write(`${kleur.bold('Usage')}\n npx cli-five add <name>\n\n`);
43
+ printAvailable();
44
+ }
45
+
46
+ function printAvailable() {
47
+ process.stdout.write(`${kleur.bold('Available add-ons')}\n`);
48
+ for (const addon of listAddons()) {
49
+ const addable = typeof addon.run === 'function';
50
+ const statusText = (addable ? 'available' : 'planned').padEnd(10);
51
+ const status = addable ? kleur.green(statusText) : kleur.yellow(statusText);
52
+ process.stdout.write(` ${addon.name.padEnd(12)} ${status} ${kleur.dim(addon.description)}\n`);
53
+ }
54
+ process.stdout.write(`\n${kleur.dim(`Known names: ${ADDON_NAMES.join(', ')}`)}\n`);
55
+ }
@@ -5,18 +5,30 @@ import { resolve, basename, join, extname } from 'node:path';
5
5
  import { log } from '../util/log.mjs';
6
6
  import { detect } from '../steps/detect.mjs';
7
7
  import { confirmOverwriteIfNeeded } from '../steps/confirm.mjs';
8
- import { interview } from '../steps/interview.mjs';
8
+ import { interview, minimalInterview } from '../steps/interview.mjs';
9
9
  import { scaffold, summarize } from '../steps/scaffold.mjs';
10
10
  import { skillDiscovery } from '../steps/skills.mjs';
11
11
  import { instructionGeneration } from '../steps/instructions.mjs';
12
- import { choosePlatform, chooseModels } from '../steps/platform.mjs';
12
+ import { choosePlatform, chooseModels, resolveCodegraphDefault } from '../steps/platform.mjs';
13
13
  import { isGitRepo, gitInit } from '../util/git.mjs';
14
14
  import { platformLabel } from '../util/platforms.mjs';
15
+ import { autoProjectInfo } from '../util/project.mjs';
15
16
 
16
17
  export async function init(args) {
17
18
  const cwd = args.cwd;
18
19
  log.raw(kleur.bold().magenta('\ncli-five init') + kleur.gray(` ${cwd}`));
19
20
 
21
+ // ── Mode ───────────────────────────────────────────────────────────
22
+ // Default init is minimal: the 5 agents + required tooling, asking only for
23
+ // platform and (when needed) name/one-liner. The legacy interview — docs,
24
+ // goals/constraints/persona, model customization, skills, instructions — is
25
+ // opt-in via --full-interview (or --doc). Per-feature flags can also force
26
+ // skills/instructions/persona without the whole interview.
27
+ const docs = Array.isArray(args.docs) ? args.docs : [];
28
+ const fullInterview = Boolean(args.fullInterview) || docs.length > 0;
29
+ const runSkills = args.skills !== null ? Boolean(args.skills) : fullInterview;
30
+ const runInstructions = args.instructions !== null ? Boolean(args.instructions) : fullInterview;
31
+
20
32
  // 1. Detect
21
33
  log.step('1/8 Detect workspace');
22
34
  const detected = detect(cwd);
@@ -27,10 +39,17 @@ export async function init(args) {
27
39
 
28
40
  // 2. Platform + CodeGraph
29
41
  log.step('2/8 Choose platform');
30
- const { platform, codegraph } = await choosePlatform(args);
42
+ const codegraphDefault = resolveCodegraphDefault(args, fullInterview);
43
+ const { platform, codegraph } = await choosePlatform(args, {
44
+ autoDetect: !fullInterview,
45
+ askCodegraph: fullInterview && args.codegraph === null,
46
+ codegraphDefault,
47
+ });
31
48
  log.info(`Target: ${kleur.bold(platformLabel(platform))}`);
32
49
  if (codegraph) log.info(`CodeGraph: ${kleur.green('enabled')}`);
33
- else log.info('CodeGraph: disabled');
50
+ else if (fullInterview || args.codegraph === false) log.info('CodeGraph: disabled');
51
+ else log.info(`CodeGraph: ${kleur.gray('disabled')} ${kleur.dim('(enable with --codegraph or --full-interview)')}`);
52
+ if (!fullInterview) log.dim('Minimal init. Full interview: npx cli-five init --full-interview');
34
53
 
35
54
  // 3. git init if needed
36
55
  if (!detected.hasGit) {
@@ -51,43 +70,57 @@ export async function init(args) {
51
70
  }
52
71
  if (!detected.hasAgents && !detected.hasCopilotInstructions) log.dim('No collisions.');
53
72
 
54
- // 5. Input mode — docs or manual interview
55
- log.step('4/8 Project info');
73
+ // 5. Project info + model configuration
74
+ args.__platform = platform;
56
75
  let docHints;
76
+ let modelConfig;
77
+
78
+ if (fullInterview) {
79
+ log.step('4/8 Project info');
80
+ if (docs.length > 0) {
81
+ // --doc was passed on the CLI — validate with retry
82
+ docHints = loadDocs(docs, cwd);
83
+ if (docHints.files.length === 0) {
84
+ log.warn('None of the --doc files could be loaded.');
85
+ }
86
+ } else if (args.yes) {
87
+ docHints = loadDocs([], cwd);
88
+ } else {
89
+ docHints = await collectDocFiles(cwd);
90
+ }
57
91
 
58
- if (args.docs.length > 0) {
59
- // --doc was passed on the CLI — validate with retry
60
- docHints = loadDocs(args.docs, cwd);
61
- if (docHints.files.length === 0) {
62
- log.warn('None of the --doc files could be loaded.');
92
+ if (docHints.files.length > 0 && docs.length > 0) {
93
+ log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
94
+ if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
95
+ if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
63
96
  }
64
- } else if (args.yes) {
65
- docHints = loadDocs([], cwd);
97
+
98
+ log.step('5/8 Model configuration');
99
+ modelConfig = await chooseModels(platform, args);
66
100
  } else {
67
- docHints = await collectDocFiles(cwd);
68
- }
101
+ log.step('4/8 Project info');
102
+ docHints = autoProjectInfo(cwd);
103
+ logAutoProjectInfo(docHints);
69
104
 
70
- if (docHints.files.length > 0 && args.docs.length > 0) {
71
- log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
72
- if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
73
- if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
105
+ log.step('5/8 Model configuration');
106
+ // Minimal path uses provider defaults without prompting (still honours --provider).
107
+ modelConfig = await chooseModels(platform, { ...args, yes: true });
74
108
  }
75
109
 
76
- // 6. Model configuration
77
- log.step('5/8 Model configuration');
78
- const modelConfig = await chooseModels(platform, args);
79
110
  log.info(`Provider: ${kleur.bold(modelConfig.provider)}`);
80
111
  if (modelConfig.customized) log.info('Models: customized');
81
112
  else log.info('Models: defaults');
82
113
 
83
- // 7. Interview (pre-filled from docs if available, otherwise manual)
84
- args.__platform = platform;
85
- const answers = await interview(detected, args, docHints);
114
+ // 6. Interview (minimal by default, full behind --full-interview)
115
+ const answers = fullInterview
116
+ ? await interview(detected, args, docHints)
117
+ : await minimalInterview(detected, args, docHints);
86
118
 
87
- // CLI --cost-mode override
119
+ // CLI overrides — apply to both paths.
88
120
  if (args.costMode && ['premium', 'cheap', 'mixed'].includes(args.costMode)) {
89
121
  answers.costMode = args.costMode;
90
122
  }
123
+ if (args.persona !== null) answers.snark = Boolean(args.persona);
91
124
 
92
125
  // Attach platform/model choices to answers so scaffold can use them.
93
126
  answers.platform = platform;
@@ -101,30 +134,56 @@ export async function init(args) {
101
134
  }
102
135
  if (answers.frameworks.length) log.info(`Stack: ${answers.stack.join(', ')} + ${answers.frameworks.join(', ')}`);
103
136
 
104
- // 8. Scaffold
137
+ // 7. Scaffold
105
138
  log.step('6/8 Scaffold');
106
139
  const written = scaffold({ cwd, answers, args });
107
140
  if (args.dryRun) log.warn('--dry-run: no files written. Plan:');
108
141
  log.raw(summarize(written, cwd));
109
142
  if (!args.dryRun) log.ok(`Wrote ${written.length} files.`);
110
143
 
111
- // 9. Skill discovery
144
+ // 8. Skill discovery
112
145
  log.step('7/8 Skill discovery');
113
- await skillDiscovery({ cwd, answers, args });
146
+ if (runSkills) {
147
+ await skillDiscovery({ cwd, answers, args: { ...args, skills: true } });
148
+ } else {
149
+ log.dim('Skipped (minimal init). Enable with --skills or --full-interview.');
150
+ }
114
151
 
115
- // 10. Custom instructions
152
+ // 9. Custom instructions
116
153
  log.step('8/8 Custom instructions');
117
- const instrWritten = await instructionGeneration({ cwd, answers, args });
118
- if (instrWritten && instrWritten.length > 0) {
119
- if (args.dryRun) log.warn('--dry-run: instruction plan:');
120
- for (const w of instrWritten) {
121
- log.raw(` ${w.written ? '+' : '~'} ${w.path.replace(cwd + '/', '')}`);
154
+ if (runInstructions) {
155
+ const instrWritten = await instructionGeneration({ cwd, answers, args });
156
+ if (instrWritten && instrWritten.length > 0) {
157
+ if (args.dryRun) log.warn('--dry-run: instruction plan:');
158
+ for (const w of instrWritten) {
159
+ log.raw(` ${w.written ? '+' : '~'} ${w.path.replace(cwd + '/', '')}`);
160
+ }
161
+ if (!args.dryRun) log.ok(`Wrote ${instrWritten.length} instruction file${instrWritten.length > 1 ? 's' : ''}.`);
122
162
  }
123
- if (!args.dryRun) log.ok(`Wrote ${instrWritten.length} instruction file${instrWritten.length > 1 ? 's' : ''}.`);
163
+ } else {
164
+ log.dim('Skipped (minimal init). Enable with --instructions or --full-interview.');
124
165
  }
125
166
 
126
- // 11. Next steps
127
- printNextSteps(answers);
167
+ // 10. Next steps
168
+ printNextSteps(answers, { generatedInstructions: runInstructions });
169
+ }
170
+
171
+ /** Log which project fields were auto-pulled from the workspace. */
172
+ function logAutoProjectInfo(info) {
173
+ const name = info?.name || {};
174
+ const oneLiner = info?.oneLiner || {};
175
+
176
+ if (name.value && !name.ambiguous) {
177
+ log.info(`Name: ${kleur.bold(name.value)} ${kleur.gray(`(${name.sources[0].source})`)}`);
178
+ } else if (name.ambiguous) {
179
+ log.warn(`Multiple project names found (${name.sources.map((s) => s.source).join(', ')}) — asking.`);
180
+ }
181
+
182
+ if (oneLiner.value && !oneLiner.ambiguous) {
183
+ log.info(`Tagline: ${oneLiner.value} ${kleur.gray(`(${oneLiner.sources[0].source})`)}`);
184
+ } else if (oneLiner.ambiguous) {
185
+ log.warn(`Multiple descriptions found (${oneLiner.sources.map((s) => s.source).join(', ')}) — asking.`);
186
+ }
128
187
  }
129
188
 
130
189
  async function ask(message, initial = false) {
@@ -132,7 +191,7 @@ async function ask(message, initial = false) {
132
191
  return Boolean(v);
133
192
  }
134
193
 
135
- function printNextSteps(answers) {
194
+ function printNextSteps(answers, { generatedInstructions = false } = {}) {
136
195
  const hasDocs = answers.docFiles?.length > 0;
137
196
  const platform = answers.platform || 'copilot';
138
197
  const codegraph = answers.codegraph;
@@ -165,8 +224,10 @@ function printNextSteps(answers) {
165
224
  } else {
166
225
  log.raw(kleur.gray(` read PROJECT.md and implement Phase 1.`));
167
226
  }
168
- log.raw(` 6. Review generated instruction files in .github/instructions/.`);
169
- log.raw(kleur.gray(` Edit applyTo globs and guidelines to fit your project.`));
227
+ if (generatedInstructions) {
228
+ log.raw(` 6. Review generated instruction files in .github/instructions/.`);
229
+ log.raw(kleur.gray(` Edit applyTo globs and guidelines to fit your project.`));
230
+ }
170
231
 
171
232
  if (codegraph) {
172
233
  log.raw('');
@@ -183,6 +244,9 @@ function printNextSteps(answers) {
183
244
  log.raw('');
184
245
  log.raw(kleur.dim('Edit agent models anytime by changing `model:` in .opencode/agents/*.md.'));
185
246
  }
247
+
248
+ log.raw('');
249
+ log.raw(kleur.dim('Optional integrations: npx cli-five list-addons'));
186
250
  log.raw('');
187
251
  }
188
252
 
@@ -0,0 +1,49 @@
1
+ import kleur from 'kleur';
2
+ import { log } from '../util/log.mjs';
3
+ import { detectAddon, listAddons } from '../addons/registry.mjs';
4
+
5
+ /**
6
+ * `cli-five list-addons` — show what is installed vs. available.
7
+ *
8
+ * "Installed" is detected read-only from workspace artifacts. Status text is
9
+ * deliberately honest: an add-on that ships partial capability (e.g. jev's
10
+ * tier-routing without the parked test-gate) must not read as fully installed.
11
+ */
12
+ export function listAddonsCommand(args) {
13
+ const cwd = args.cwd;
14
+ log.raw(kleur.bold().magenta('\ncli-five list-addons') + kleur.gray(` ${cwd}`));
15
+ log.raw('');
16
+
17
+ log.raw(` ${kleur.gray(pad('ADD-ON', 12))} ${kleur.gray(pad('STATUS', 14))} ${kleur.gray(pad('ADD', 10))} ${kleur.gray('DETAIL')}`);
18
+ log.raw(` ${'─'.repeat(12)} ${'─'.repeat(14)} ${'─'.repeat(10)} ${'─'.repeat(30)}`);
19
+
20
+ for (const addon of listAddons()) {
21
+ const signals = detectAddon(addon, cwd);
22
+ const installed = signals.length > 0;
23
+ const addable = typeof addon.run === 'function';
24
+
25
+ // An installed add-on with a `capability` note is partial — say so.
26
+ const statusLabel = installed
27
+ ? (addon.capability ? `installed (${addon.capability})` : 'installed')
28
+ : 'not found';
29
+
30
+ const detail = installed
31
+ ? [addon.status || signals.join(', ')].filter(Boolean).join(' — ')
32
+ : addon.note || addon.status || '';
33
+
34
+ const statusText = pad(statusLabel, 14);
35
+ const status = installed ? kleur.green(statusText) : kleur.gray(statusText);
36
+ const addableText = pad(addable ? 'available' : 'planned', 10);
37
+ const addableColored = addable ? kleur.green(addableText) : kleur.yellow(addableText);
38
+
39
+ log.raw(` ${pad(addon.name, 12)} ${status} ${addableColored} ${kleur.dim(detail)}`);
40
+ }
41
+
42
+ log.raw('');
43
+ log.dim('Install with `npx cli-five add <name>`.');
44
+ log.raw('');
45
+ }
46
+
47
+ function pad(value, width) {
48
+ return String(value).padEnd(width);
49
+ }
@@ -195,6 +195,65 @@ export async function interview(detected, args, docHints = {}) {
195
195
  });
196
196
  }
197
197
 
198
+ /**
199
+ * Minimal interview — the default `init` path.
200
+ *
201
+ * Asks for the project name and one-liner only, and only when `projectInfo`
202
+ * could not confidently supply them. Everything else (stack, goals,
203
+ * constraints, persona, cost mode) falls back to `defaults`.
204
+ *
205
+ * `projectInfo` is the shape returned by `autoProjectInfo(cwd)`.
206
+ */
207
+ export async function minimalInterview(detected, args, projectInfo = {}) {
208
+ const platform = args.__platform || 'copilot';
209
+ const nameInfo = projectInfo.name || {};
210
+ const oneLinerInfo = projectInfo.oneLiner || {};
211
+
212
+ let projectName = nameInfo.value || '';
213
+ let oneLiner = oneLinerInfo.value || '';
214
+
215
+ if (!args.yes) {
216
+ const questions = [];
217
+
218
+ if (!projectName || nameInfo.ambiguous) {
219
+ questions.push({
220
+ type: 'text',
221
+ name: 'projectName',
222
+ message: 'Project name',
223
+ initial: projectName || detected.projectName,
224
+ });
225
+ }
226
+
227
+ if (!oneLiner || oneLinerInfo.ambiguous) {
228
+ questions.push({
229
+ type: 'text',
230
+ name: 'oneLiner',
231
+ message: 'One-line description (becomes PROJECT.md vision)',
232
+ initial: oneLiner || '',
233
+ });
234
+ }
235
+
236
+ if (questions.length > 0) {
237
+ const answers = await prompts(questions, {
238
+ onCancel: () => {
239
+ throw new Error('Interview cancelled. Nothing was written.');
240
+ },
241
+ });
242
+ if (answers.projectName !== undefined) projectName = answers.projectName;
243
+ if (answers.oneLiner !== undefined) oneLiner = answers.oneLiner;
244
+ }
245
+ }
246
+
247
+ const base = defaults(detected, { projectName, oneLiner }, platform);
248
+ return normalize({
249
+ ...base,
250
+ projectName: (projectName || detected.projectName || '').trim(),
251
+ oneLiner: (oneLiner || '').trim(),
252
+ // Persona is opt-in on the minimal path (--persona / --full-interview).
253
+ snark: args.persona === true,
254
+ });
255
+ }
256
+
198
257
  /** Default stack is first preset when nothing is detected and --yes is used. */
199
258
  function defaults(detected, docHints = {}, platform = 'copilot') {
200
259
  const hasDetected = detected.stacks.length > 0;
@@ -1,5 +1,7 @@
1
1
  import kleur from 'kleur';
2
2
  import prompts from 'prompts';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
3
5
  import {
4
6
  PLATFORM_COPILOT,
5
7
  PLATFORM_OPENCODE,
@@ -21,20 +23,50 @@ import {
21
23
 
22
24
  const CUSTOM_SENTINEL = '__custom__';
23
25
 
26
+ /**
27
+ * Resolve whether CodeGraph should be enabled when no explicit `--codegraph`
28
+ * / `--no-codegraph` value was passed.
29
+ *
30
+ * The registration/opt-out mechanism is unchanged — this only decides the
31
+ * default: on for the full interview, off for minimal init.
32
+ */
33
+ export function resolveCodegraphDefault(args, fullInterview) {
34
+ if (args.codegraph === true || args.codegraph === false) return args.codegraph;
35
+ return Boolean(fullInterview);
36
+ }
37
+
24
38
  /**
25
39
  * Ask the user to choose a target platform.
26
40
  * If args.target is a valid platform, skip the prompt.
41
+ *
42
+ * Options:
43
+ * autoDetect — infer the platform from an existing scaffold before prompting
44
+ * askCodegraph — whether to ask the CodeGraph opt-out question (full interview)
45
+ * codegraphDefault — resolved default when no explicit flag was passed
46
+ *
47
+ * Note: CodeGraph registration and the `--no-codegraph` opt-out are unchanged;
48
+ * this only controls whether the question is asked / what the default is.
27
49
  */
28
- export async function choosePlatform(args) {
50
+ export async function choosePlatform(args, { autoDetect = false, askCodegraph = true, codegraphDefault } = {}) {
51
+ if (codegraphDefault === undefined) codegraphDefault = args.codegraph !== false;
52
+
29
53
  if (args.target) {
30
54
  const t = String(args.target).toLowerCase();
31
55
  if (PLATFORMS.includes(t)) {
32
- return { platform: t, codegraph: args.codegraph !== false };
56
+ return { platform: t, codegraph: codegraphDefault };
57
+ }
58
+ }
59
+
60
+ if (autoDetect) {
61
+ const existing = detectExistingPlatform(args.cwd);
62
+ if (existing) {
63
+ log.dim(`Detected existing ${platformLabel(existing)} scaffold.`);
64
+ return { platform: existing, codegraph: codegraphDefault };
33
65
  }
34
66
  }
35
67
 
36
68
  if (args.yes) {
37
- return { platform: PLATFORM_COPILOT, codegraph: args.codegraph !== false };
69
+ return { platform: PLATFORM_COPILOT, codegraph: codegraphDefault };
38
70
  }
39
71
 
40
72
  const { platform } = await prompts({
@@ -60,6 +92,10 @@ export async function choosePlatform(args) {
60
92
  throw new Error('Platform selection cancelled. Nothing was written.');
61
93
  }
62
94
 
95
+ if (!askCodegraph) {
96
+ return { platform, codegraph: codegraphDefault };
97
+ }
98
+
63
99
  const { codegraph } = await prompts({
64
100
  type: 'confirm',
65
101
  name: 'codegraph',
@@ -70,6 +106,13 @@ export async function choosePlatform(args) {
70
106
  return { platform, codegraph: codegraph !== false };
71
107
  }
72
108
 
109
+ /** Infer an existing scaffold's platform, or null when there is no scaffold. */
110
+ export function detectExistingPlatform(cwd) {
111
+ if (existsSync(join(cwd, '.opencode', 'agents'))) return PLATFORM_OPENCODE;
112
+ if (existsSync(join(cwd, '.github', 'agents', 'orchestrator.agent.md'))) return PLATFORM_COPILOT;
113
+ return null;
114
+ }
115
+
73
116
  /**
74
117
  * Ask the user whether to customize models, pick a provider, and optionally
75
118
  * override per-agent models.
@@ -3,6 +3,7 @@ import { log } from '../util/log.mjs';
3
3
  import { readTemplate, render, writeFile, listFilesRecursive, relTo, templatePath } from '../util/fs.mjs';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { PLATFORM_COPILOT, PLATFORM_OPENCODE, agentDirFor, agentFileFor } from '../util/platforms.mjs';
6
+ import { addCodegraphTo } from '../addons/codegraph.mjs';
6
7
 
7
8
  const AGENT_NAMES = ['orchestrator', 'planner', 'coder', 'designer', 'reviewer'];
8
9
  const HISTORY_FILES = ['orchestrator.md', 'planner.md', 'coder.md', 'designer.md', 'reviewer.md'];
@@ -44,6 +45,9 @@ export function scaffold({ cwd, answers, args }) {
44
45
  }
45
46
 
46
47
  // Shared memory primitives
48
+ // NOTE: AGENTS.md is written WITHOUT the CodeGraph block here. When CodeGraph
49
+ // is enabled, the block is merged in afterward by addCodegraphTo() — the same
50
+ // code path `add codegraph` uses (one implementation, two entry points).
47
51
  for (const tmpl of [
48
52
  'AGENTS.md.tmpl',
49
53
  'PROJECT.md.tmpl',
@@ -61,6 +65,13 @@ export function scaffold({ cwd, answers, args }) {
61
65
  written.push(writeFile(join(cwd, 'histories', file), readTemplate('histories', file), args));
62
66
  }
63
67
 
68
+ // CodeGraph — delegated to the shared add codegraph implementation.
69
+ if (answers.codegraph) {
70
+ for (const t of addCodegraphTo({ cwd, platform, dryRun: args.dryRun })) {
71
+ written.push({ path: t.path, written: !args.dryRun && t.action !== 'unchanged' });
72
+ }
73
+ }
74
+
64
75
  return written;
65
76
  }
66
77
 
@@ -93,11 +104,6 @@ function scaffoldCopilot({ cwd, answers, args, vars }) {
93
104
  writeFile(join(cwd, '.github', 'skills', 'README.md'), readTemplate('.github', 'skills', 'README.md'), args),
94
105
  );
95
106
 
96
- // CodeGraph MCP for VS Code Copilot
97
- if (answers.codegraph) {
98
- written.push(writeFile(join(cwd, '.vscode', 'mcp.json'), JSON.stringify(codegraphMcpJson(), null, 2), args));
99
- }
100
-
101
107
  return written;
102
108
  }
103
109
 
@@ -144,34 +150,10 @@ function buildOpencodeConfig({ answers, orchestratorModel }) {
144
150
  subagent_depth: 2,
145
151
  };
146
152
 
147
- if (answers.codegraph) {
148
- config.mcp = {
149
- codegraph: {
150
- type: 'local',
151
- command: ['codegraph', 'serve', '--mcp'],
152
- enabled: true,
153
- },
154
- };
155
- }
156
-
157
153
  return config;
158
154
  }
159
155
 
160
- function codegraphMcpJson() {
161
- return {
162
- inputs: [],
163
- servers: {
164
- codegraph: {
165
- command: 'codegraph',
166
- args: ['serve', '--mcp'],
167
- },
168
- },
169
- };
170
- }
171
-
172
156
  function buildVars(a) {
173
- const codegraphBlock = a.codegraph ? CODEGRAPH_BLOCK : '';
174
-
175
157
  return {
176
158
  PROJECT_NAME: a.projectName,
177
159
  ONE_LINER: a.oneLiner || 'TODO — write a one-line vision statement.',
@@ -190,7 +172,6 @@ ${a.docs}
190
172
  ` : '',
191
173
  DATE: new Date().toISOString().slice(0, 10),
192
174
  PERSONA_BLOCK: a.snark ? PERSONA_BLOCK : '',
193
- CODEGRAPH_BLOCK: codegraphBlock,
194
175
  };
195
176
  }
196
177
 
@@ -221,22 +202,6 @@ const PERSONA_BLOCK = `# Persona
221
202
 
222
203
  `;
223
204
 
224
- const CODEGRAPH_BLOCK = `
225
- <!-- CODEGRAPH_START -->
226
- ## CodeGraph
227
-
228
- This project is configured to use [CodeGraph](https://codegraph.ru) for graph-backed codebase context.
229
- When you need to understand relationships, call paths, or impacts, use:
230
-
231
- \`\`\`
232
- codegraph explore "<your question>"
233
- \`\`\`
234
-
235
- The CodeGraph MCP server is registered in the project config. Run \`codegraph init\` in this directory
236
- if the project has not been indexed yet.
237
- <!-- CODEGRAPH_END -->
238
- `;
239
-
240
205
  export function summarize(written, cwd) {
241
206
  const lines = [];
242
207
  for (const w of written) {