codehere 0.6.0 → 0.6.2

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.
Files changed (30) hide show
  1. package/README.md +8 -8
  2. package/dist/application/use-cases/generate-final-report.js +57 -2
  3. package/dist/application/use-cases/generate-narration.js +9 -3
  4. package/dist/index.js +34 -4
  5. package/dist/infrastructure/cost/budget-config.js +1 -1
  6. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  7. package/dist/infrastructure/hooks/claude-sdk-hook.js +48 -2
  8. package/dist/infrastructure/orchestration/codex-adapter.js +11 -6
  9. package/dist/infrastructure/orchestration/context-handoff.js +1 -1
  10. package/dist/infrastructure/orchestration/gemini-adapter.js +3 -15
  11. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  12. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  13. package/dist/infrastructure/orchestration/opencode-adapter.js +42 -1
  14. package/dist/infrastructure/orchestration/progress-aggregator.js +1 -2
  15. package/dist/infrastructure/orchestration/session-manager.js +178 -76
  16. package/dist/infrastructure/protocols/cloud-sync.js +9 -5
  17. package/dist/infrastructure/protocols/mcp-server-impl.js +368 -334
  18. package/dist/infrastructure/storage/cross-tool-agents.js +19 -2
  19. package/dist/infrastructure/storage/project-thread-migration.js +12 -7
  20. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  21. package/dist/infrastructure/storage/project-thread-title-backfill.js +6 -2
  22. package/dist/presentation/cli/commands/budget-command.js +2 -2
  23. package/dist/presentation/cli/commands/init-command.js +5 -4
  24. package/dist/presentation/cli/commands/memory-command.js +29 -15
  25. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  26. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  27. package/dist/presentation/cli/repl.js +20 -1
  28. package/dist/ui.js +36 -28
  29. package/dist/utils/typo-guard.js +1 -1
  30. package/package.json +2 -1
@@ -143,7 +143,10 @@ export function parseAgentFile(filePath, defaultFormat = 'unknown') {
143
143
  // Tools normalization
144
144
  let tools = null;
145
145
  if (typeof fm.tools === 'string') {
146
- tools = fm.tools.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
146
+ tools = fm.tools
147
+ .split(',')
148
+ .map((t) => t.trim().toLowerCase())
149
+ .filter(Boolean);
147
150
  }
148
151
  else if (hasObjectTools) {
149
152
  tools = [];
@@ -162,7 +165,21 @@ export function parseAgentFile(filePath, defaultFormat = 'unknown') {
162
165
  ? 'subagent'
163
166
  : 'all';
164
167
  // Track unknown frontmatter keys (everything outside our normalized set)
165
- const knownKeys = new Set(['name', 'description', 'mode', 'model', 'temperature', 'tools', 'permission', 'color', 'hidden', 'top_p', 'steps', 'prompt', 'disable']);
168
+ const knownKeys = new Set([
169
+ 'name',
170
+ 'description',
171
+ 'mode',
172
+ 'model',
173
+ 'temperature',
174
+ 'tools',
175
+ 'permission',
176
+ 'color',
177
+ 'hidden',
178
+ 'top_p',
179
+ 'steps',
180
+ 'prompt',
181
+ 'disable',
182
+ ]);
166
183
  const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
167
184
  return {
168
185
  name: typeof fm.name === 'string' ? fm.name : filenameName,
@@ -32,12 +32,7 @@ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync
32
32
  import { join } from 'path';
33
33
  import { createHash } from 'crypto';
34
34
  /** Path patterns considered ephemeral and skipped from project list. */
35
- const DEFAULT_TEMP_DIR_PATTERNS = [
36
- /^\/tmp\//,
37
- /^\/var\/folders\//,
38
- /^\/private\/var\/folders\//,
39
- /^\/private\/tmp\//,
40
- ];
35
+ const DEFAULT_TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
41
36
  /** sha1(cwd)[:12] — stable, short, filesystem-safe. */
42
37
  function slugForCwd(cwd, unknownLabel) {
43
38
  if (!cwd)
@@ -79,7 +74,17 @@ function rollupSessions(events) {
79
74
  completedIds.add(id);
80
75
  const existing = byId.get(id) ?? { id, abandoned: true };
81
76
  const existingAny = existing;
82
- for (const k of ['agent', 'prompt', 'cwd', 'startedAt', 'status', 'durationMs', 'completedAt', 'steps', 'artifacts']) {
77
+ for (const k of [
78
+ 'agent',
79
+ 'prompt',
80
+ 'cwd',
81
+ 'startedAt',
82
+ 'status',
83
+ 'durationMs',
84
+ 'completedAt',
85
+ 'steps',
86
+ 'artifacts',
87
+ ]) {
83
88
  if (e[k] !== undefined && existingAny[k] === undefined) {
84
89
  existingAny[k] = e[k];
85
90
  }
@@ -133,7 +133,17 @@ export function readProjectThread(slug, options) {
133
133
  };
134
134
  // Merge fields — later entries override earlier (covers
135
135
  // spawn → completion update pattern from forward-mirror)
136
- for (const k of ['agent', 'prompt', 'cwd', 'startedAt', 'status', 'durationMs', 'completedAt', 'steps', 'artifacts']) {
136
+ for (const k of [
137
+ 'agent',
138
+ 'prompt',
139
+ 'cwd',
140
+ 'startedAt',
141
+ 'status',
142
+ 'durationMs',
143
+ 'completedAt',
144
+ 'steps',
145
+ 'artifacts',
146
+ ]) {
137
147
  if (rec[k] !== undefined) {
138
148
  existing[k] = rec[k];
139
149
  }
@@ -178,10 +178,14 @@ export async function backfillThreadTitles(options) {
178
178
  break;
179
179
  }
180
180
  }
181
- catch { /* skip malformed */ }
181
+ catch {
182
+ /* skip malformed */
183
+ }
182
184
  }
183
185
  }
184
- catch { /* if we can't read, fall through and let the append fail */ }
186
+ catch {
187
+ /* if we can't read, fall through and let the append fail */
188
+ }
185
189
  if (userBeatUs) {
186
190
  result.alreadyTitled++;
187
191
  continue;
@@ -39,7 +39,7 @@ function showBudget() {
39
39
  const cfg = loadBudget();
40
40
  const spend = computeSpendToday();
41
41
  newline();
42
- console.log(info('Today\'s estimated spend:'), fmtUsd(spend.totalUsd), `(${spend.sessionCount} session${spend.sessionCount === 1 ? '' : 's'})`);
42
+ console.log(info("Today's estimated spend:"), fmtUsd(spend.totalUsd), `(${spend.sessionCount} session${spend.sessionCount === 1 ? '' : 's'})`);
43
43
  if (cfg.dailyUsd === null) {
44
44
  console.log(info('Daily budget:'), 'none — no enforcement');
45
45
  console.log(info('Set one with:'), 'codehere budget set 5');
@@ -67,7 +67,7 @@ function setBudget(value) {
67
67
  cfg.dailyUsd = n;
68
68
  saveBudget(cfg);
69
69
  console.log(success('Daily budget set to'), fmtUsd(n));
70
- console.log(info('Spawns will be blocked once today\'s estimated spend reaches this amount.'));
70
+ console.log(info("Spawns will be blocked once today's estimated spend reaches this amount."));
71
71
  console.log(info('Disable with:'), 'codehere budget off');
72
72
  }
73
73
  function disableBudget() {
@@ -39,7 +39,7 @@
39
39
  * Refuses to follow symlinks (lstat check) so a hostile/misplaced symlink at
40
40
  * `.codehere/CONTEXT.md` or `.gitignore` cannot redirect writes outside the repo.
41
41
  */
42
- import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, lstatSync, statSync, symlinkSync, readlinkSync, rmSync } from 'fs';
42
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, lstatSync, statSync, symlinkSync, readlinkSync, rmSync, } from 'fs';
43
43
  import { homedir } from 'os';
44
44
  import { join, dirname } from 'path';
45
45
  import { colors, success, info, warning, error, newline } from '../../../ui.js';
@@ -316,7 +316,7 @@ function findSymlinkAncestor(codehereDir, relativePath) {
316
316
  // Defensive: reject any relative path containing .. — even though templates
317
317
  // are hard-coded, this protects against future schema additions slipping a
318
318
  // traversal through.
319
- if (segments.some(seg => seg === '..' || seg === '')) {
319
+ if (segments.some((seg) => seg === '..' || seg === '')) {
320
320
  return relativePath;
321
321
  }
322
322
  // Walk codehereDir, codehereDir/seg1, codehereDir/seg1/seg2, ... (excluding
@@ -338,12 +338,13 @@ function ensureGitignore(repoRoot) {
338
338
  return { result: 'refused-symlink', addedLines: 0 };
339
339
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
340
340
  // De-duplicate: only append lines that aren't already present.
341
- const missing = GITIGNORE_LINES.filter(line => !existing.includes(line));
341
+ const missing = GITIGNORE_LINES.filter((line) => !existing.includes(line));
342
342
  if (missing.length === 0)
343
343
  return { result: 'already-present', addedLines: 0 };
344
344
  const block = (existing.length > 0 && !existing.endsWith('\n') ? '\n' : '') +
345
345
  `\n${GITIGNORE_MARKER}\n` +
346
- missing.join('\n') + '\n';
346
+ missing.join('\n') +
347
+ '\n';
347
348
  appendFileSync(gitignorePath, block);
348
349
  return { result: 'updated', addedLines: missing.length };
349
350
  }
@@ -48,7 +48,7 @@ import { buildMemoryPrefix } from '../../../infrastructure/orchestration/memory-
48
48
  * Built lazily from init-command.ts's F3_TEMPLATES so the seed bodies stay
49
49
  * in one place — no duplication, no drift.
50
50
  */
51
- const DEFAULT_SEED_BODIES = new Map(F3_TEMPLATES.map(t => [t.filename, t.body]));
51
+ const DEFAULT_SEED_BODIES = new Map(F3_TEMPLATES.map((t) => [t.filename, t.body]));
52
52
  function lessonIdForText(text) {
53
53
  return createHash('sha256').update(text).digest('hex').slice(0, 16);
54
54
  }
@@ -58,8 +58,8 @@ function readLessonsFile(path) {
58
58
  return readFileSync(path, 'utf8')
59
59
  .trim()
60
60
  .split('\n')
61
- .filter(l => l.length > 0)
62
- .map(l => {
61
+ .filter((l) => l.length > 0)
62
+ .map((l) => {
63
63
  try {
64
64
  return JSON.parse(l);
65
65
  }
@@ -83,7 +83,7 @@ function readLessonsFile(path) {
83
83
  function appendLessonIfNew(path, text, source) {
84
84
  const id = lessonIdForText(text);
85
85
  const existing = readLessonsFile(path);
86
- const dup = existing.find(l => l.id === id);
86
+ const dup = existing.find((l) => l.id === id);
87
87
  if (dup)
88
88
  return { result: 'duplicate', lesson: dup };
89
89
  const lesson = {
@@ -326,7 +326,9 @@ function gatherLayerSummary(codehereDir, projectRoot) {
326
326
  const episodicLearnings = join(codehereDir, 'memory/episodic/AGENT_LEARNINGS.jsonl');
327
327
  const repoMapPath = join(codehereDir, 'memory/episodic/repo_map.json');
328
328
  const learningsLine = existsSync(episodicLearnings) || isSymlink(episodicLearnings)
329
- ? (isSymlink(episodicLearnings) ? `AGENT_LEARNINGS.jsonl → ${readlinkSync(episodicLearnings)}` : 'AGENT_LEARNINGS.jsonl (real file)')
329
+ ? isSymlink(episodicLearnings)
330
+ ? `AGENT_LEARNINGS.jsonl → ${readlinkSync(episodicLearnings)}`
331
+ : 'AGENT_LEARNINGS.jsonl (real file)'
330
332
  : '(no AGENT_LEARNINGS.jsonl)';
331
333
  const repoMapLine = existsSync(repoMapPath)
332
334
  ? `repo_map.json (${fmtSize(statSync(repoMapPath).size)})`
@@ -354,9 +356,12 @@ function gatherLayerSummary(codehereDir, projectRoot) {
354
356
  const skillsDir = join(codehereDir, 'skills');
355
357
  if (existsSync(skillsDir)) {
356
358
  const subdirs = readdirSync(skillsDir, { withFileTypes: true })
357
- .filter(e => e.isDirectory())
358
- .filter(e => existsSync(join(skillsDir, e.name, 'SKILL.md')));
359
- out.push({ layer: 'skills', line: `skills · ${subdirs.length} skill folder${subdirs.length === 1 ? '' : 's'}` });
359
+ .filter((e) => e.isDirectory())
360
+ .filter((e) => existsSync(join(skillsDir, e.name, 'SKILL.md')));
361
+ out.push({
362
+ layer: 'skills',
363
+ line: `skills · ${subdirs.length} skill folder${subdirs.length === 1 ? '' : 's'}`,
364
+ });
360
365
  }
361
366
  else {
362
367
  out.push({ layer: 'skills', line: `skills · (no skills/ dir)` });
@@ -368,7 +373,10 @@ function gatherLayerSummary(codehereDir, projectRoot) {
368
373
  const parsed = JSON.parse(readFileSync(patternsPath, 'utf8'));
369
374
  const builtin = Array.isArray(parsed.patterns_builtin) ? parsed.patterns_builtin.length : 0;
370
375
  const project = Array.isArray(parsed.patterns_project) ? parsed.patterns_project.length : 0;
371
- out.push({ layer: 'protocols', line: `protocols · hook_patterns.json (${builtin} built-in + ${project} project)` });
376
+ out.push({
377
+ layer: 'protocols',
378
+ line: `protocols · hook_patterns.json (${builtin} built-in + ${project} project)`,
379
+ });
372
380
  }
373
381
  catch {
374
382
  out.push({ layer: 'protocols', line: `protocols · hook_patterns.json (parse error)` });
@@ -389,7 +397,10 @@ function gatherLayerSummary(codehereDir, projectRoot) {
389
397
  }
390
398
  else {
391
399
  const truncNote = inj.truncated ? ' TRUNCATED' : '';
392
- out.push({ layer: 'injection', line: `injection · ${inj.tokens} tokens · ${inj.included.join(', ')}${truncNote}` });
400
+ out.push({
401
+ layer: 'injection',
402
+ line: `injection · ${inj.tokens} tokens · ${inj.included.join(', ')}${truncNote}`,
403
+ });
393
404
  }
394
405
  return out;
395
406
  }
@@ -526,7 +537,10 @@ function executeMemoryShow(layerArg) {
526
537
  console.log(` ${info('empty ')} no lessons.jsonl yet`);
527
538
  break;
528
539
  }
529
- const lines = readFileSync(path, 'utf8').trim().split('\n').filter(l => l.length > 0);
540
+ const lines = readFileSync(path, 'utf8')
541
+ .trim()
542
+ .split('\n')
543
+ .filter((l) => l.length > 0);
530
544
  if (lines.length === 0) {
531
545
  console.log(` ${info('empty ')} lessons.jsonl is empty — run ${colors.cyan('codehere memory graduate')} (coming in v0.5)`);
532
546
  break;
@@ -561,8 +575,8 @@ function executeMemoryShow(layerArg) {
561
575
  break;
562
576
  }
563
577
  const subdirs = readdirSync(skillsDir, { withFileTypes: true })
564
- .filter(e => e.isDirectory())
565
- .filter(e => existsSync(join(skillsDir, e.name, 'SKILL.md')));
578
+ .filter((e) => e.isDirectory())
579
+ .filter((e) => existsSync(join(skillsDir, e.name, 'SKILL.md')));
566
580
  if (subdirs.length === 0) {
567
581
  console.log(` ${info('empty ')} no skill folders yet (skills/<name>/SKILL.md)`);
568
582
  break;
@@ -907,8 +921,8 @@ function executeMemoryInit(options) {
907
921
  }
908
922
  // Idempotency hint when nothing was actually migrated.
909
923
  const allOutcomes = [contextOutcome, handoverOutcome, promptsResult.outcome];
910
- const anyMigrated = allOutcomes.some(o => o === 'migrated');
911
- const allAlreadyOrMissing = allOutcomes.every(o => o === 'already-migrated' || o === 'missing');
924
+ const anyMigrated = allOutcomes.some((o) => o === 'migrated');
925
+ const allAlreadyOrMissing = allOutcomes.every((o) => o === 'already-migrated' || o === 'missing');
912
926
  if (!anyMigrated && allAlreadyOrMissing) {
913
927
  newline();
914
928
  console.log(` ${info('no-op')} everything already migrated or no source files to migrate.`);
@@ -122,8 +122,8 @@ export function buildHookPatternsExport() {
122
122
  source: 'agent/src/infrastructure/security/trust-scorer.ts',
123
123
  _note: "Exported from codehere's trust-scorer pattern bank via 'codehere protocols export'. " +
124
124
  "DO NOT hand-edit `patterns_builtin[]` — it's regenerated on every export. " +
125
- "Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. " +
126
- "Pattern ids are stable slugs of category + description; reference them deterministically.",
125
+ 'Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. ' +
126
+ 'Pattern ids are stable slugs of category + description; reference them deterministically.',
127
127
  patterns_builtin: builtin,
128
128
  patterns_project: [],
129
129
  };
@@ -107,8 +107,8 @@ function gitListFiles(repoRoot) {
107
107
  return null;
108
108
  return r.stdout
109
109
  .split('\0')
110
- .filter(p => p.length > 0)
111
- .filter(p => SUPPORTED_EXTENSIONS.has(extname(p)));
110
+ .filter((p) => p.length > 0)
111
+ .filter((p) => SUPPORTED_EXTENSIONS.has(extname(p)));
112
112
  }
113
113
  /**
114
114
  * Count references to a symbol name across the repo via git grep.
@@ -191,13 +191,13 @@ export function buildRepoMap(repoRoot) {
191
191
  // acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
192
192
  // v0.6's tree-sitter port will swap raw grep for AST-aware reference
193
193
  // resolution + PageRank, which is faster AND more accurate.
194
- const uniqueNames = Array.from(new Set(allSymbols.map(s => s.name)));
194
+ const uniqueNames = Array.from(new Set(allSymbols.map((s) => s.name)));
195
195
  const refCounts = new Map();
196
196
  for (const name of uniqueNames) {
197
197
  refCounts.set(name, countReferences(repoRoot, name));
198
198
  }
199
199
  const topSymbols = allSymbols
200
- .map(s => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
200
+ .map((s) => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
201
201
  // Stable sort: references DESC, then name ASC for tie-breaking.
202
202
  .sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
203
203
  .slice(0, 50);
@@ -648,7 +648,24 @@ async function runSubcommand(name, extraArgs) {
648
648
  });
649
649
  });
650
650
  }
651
+ /**
652
+ * UAT-002 — exit-intent words typed at the REPL prompt without a leading
653
+ * slash should leave the session, not get routed to a Conductor spawn.
654
+ *
655
+ * Pre-fix: typing bare `exit` (no `/`) fell through to spawnViaConductor,
656
+ * which booted Conductor, picked claude-code under default routing, made
657
+ * a real API call (network or quota), and emitted a confusing Final Report
658
+ * echoing the provider error verbatim. First-impression UX bug found by
659
+ * UAT 2026-05-19.
660
+ */
661
+ const EXIT_INTENT_WORDS = new Set(['exit', 'quit', 'q', 'bye', 'goodbye', 'logout', ':q']);
651
662
  async function spawnViaConductor(prompt, state) {
663
+ // UAT-002 guard — exit-intent before any spawn / API call.
664
+ const trimmed = prompt.trim().toLowerCase();
665
+ if (EXIT_INTENT_WORDS.has(trimmed)) {
666
+ console.log(colors.dim('Goodbye! 👋'));
667
+ process.exit(0);
668
+ }
652
669
  const installed = availableAgents().filter((a) => a.installed);
653
670
  if (installed.length === 0) {
654
671
  console.log(warning('No specialist installed — install one and try again.'));
@@ -711,7 +728,9 @@ function renderEvent(event) {
711
728
  console.log(colors.dim(' │ ') + icon + ' ' + label);
712
729
  // P22 / Track B #8 — render last 3 lines of step output for done/failed
713
730
  if (lastStep.output && String(lastStep.output).trim()) {
714
- const outLines = String(lastStep.output).split(String.fromCharCode(10)).filter(l => l.trim());
731
+ const outLines = String(lastStep.output)
732
+ .split(String.fromCharCode(10))
733
+ .filter((l) => l.trim());
715
734
  const preview = outLines.slice(-3);
716
735
  for (const ln of preview) {
717
736
  console.log(colors.dim(' │ ') + colors.gray(ln));
package/dist/ui.js CHANGED
@@ -397,37 +397,45 @@ export function randomFarewell() {
397
397
  export function randomCleanScan() {
398
398
  return CLEAN_SCAN_CELEBRATIONS[Math.floor(Math.random() * CLEAN_SCAN_CELEBRATIONS.length)];
399
399
  }
400
+ /**
401
+ * v0.6.2 sync — every non-replOnly entry MUST correspond to a real
402
+ * `program.command('<name>')` registration in src/index.ts. Drift is
403
+ * blocked by test/cli/slash-commands-sync.test.ts (UAT-001 CI guard).
404
+ *
405
+ * Pre-fix (v0.6.1 and earlier): this list carried 16 ghost commands
406
+ * (ask/explain/fix/review/plan/suggest/test/docs/refactor/orchestrate/run/
407
+ * git-review/index/cost/models/undo) removed in v0.3 — and was missing
408
+ * 8 real commands (init/memory/permission/narrator/budget/start/task/kill).
409
+ * Catastrophic first-impression bug found by UAT 2026-05-19.
410
+ */
400
411
  export const SLASH_COMMANDS = [
401
- // AI-powered
402
- { name: 'ask', args: '<question>', description: 'Ask anything about your codebase' },
403
- { name: 'explain', args: '<file>', description: 'Explain a file' },
404
- { name: 'fix', args: '<file> <hint>', description: 'Apply a targeted fix' },
405
- { name: 'review', args: '<file>', description: 'Code review with findings' },
406
- { name: 'plan', args: '<goal>', description: 'Break a goal into steps' },
407
- { name: 'suggest', args: '<query>', description: 'Read-only assistant — no changes' },
408
- { name: 'test', args: '<file>', description: 'Generate tests for a file' },
409
- { name: 'docs', args: '<type>', description: 'Generate documentation' },
410
- { name: 'refactor', args: '<pattern> <instr>', description: 'Refactor across files' },
411
- { name: 'orchestrate', args: '<goal>', description: 'Multi-agent workflow' },
412
- { name: 'run', args: '<goal>', description: 'Execute a task or goal' },
413
- { name: 'git-review', args: '', description: 'Review git changes' },
414
- // Safety & config (no key)
415
- { name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', noKey: true },
416
- { name: 'report', args: '[path]', description: 'Security audit report', noKey: true },
417
- { name: 'hook', args: '[install|status]', description: 'Safety hook management', noKey: true },
418
- { name: 'status', args: '', description: 'Dashboard — hook activity', noKey: true },
419
- { name: 'health', args: '', description: 'System health check', noKey: true },
420
- { name: 'index', args: '', description: 'Build the search index' },
421
- { name: 'serve', args: '', description: 'Start MCP server', noKey: true },
422
- { name: 'setup', args: '', description: 'Configure API provider', noKey: true },
412
+ // Setup + config (no key)
413
+ { name: 'setup', args: '', description: 'Interactive first-run wizard', noKey: true },
414
+ { name: 'init', args: '', description: 'Scaffold .codehere/ in this repo', noKey: true },
415
+ { name: 'memory', args: '<sub>', description: 'Project memory · init / repo-map / show', noKey: true },
423
416
  { name: 'config', args: '[key] [value]', description: 'Manage preferences', noKey: true },
424
- { name: 'cost', args: '', description: 'View API cost tracking', noKey: true },
425
- { name: 'models', args: '', description: 'List available AI models', noKey: true },
426
- { name: 'undo', args: '<file>', description: 'Undo file changes', noKey: true },
427
- // Utility
417
+ { name: 'permission', args: '<sub>', description: 'Manage .codehere/permissions.yaml allow/deny', noKey: true },
418
+ { name: 'narrator', args: '', description: 'Customize the PM narrator prompt', noKey: true },
419
+ { name: 'budget', args: '[set <amount>]', description: 'Daily spend budget', noKey: true },
420
+ // Safety + observability (no key)
421
+ { name: 'hook', args: '[install|status]', description: 'Safety hook management', noKey: true },
422
+ { name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', noKey: true },
423
+ { name: 'report', args: '[path]', description: 'Shareable security audit', noKey: true },
424
+ { name: 'status', args: '', description: 'Security dashboard — hook activity', noKey: true },
425
+ { name: 'health', args: '', description: 'System health + metrics', noKey: true },
426
+ // Orchestration
427
+ { name: 'start', args: '', description: 'Open the dashboard at localhost:3773' },
428
+ { name: 'serve', args: '', description: 'Run headless MCP server (no UI)', noKey: true },
429
+ { name: 'task', args: '<prompt>', description: 'Delegate a task to a coding agent' },
430
+ { name: 'kill', args: '<sessionId>', description: 'Kill a running task' },
431
+ // Utility (no key)
428
432
  { name: 'help', args: '', description: 'Command reference', noKey: true },
429
- { name: 'clear', args: '', description: 'Clear the screen', noKey: true },
430
- { name: 'exit', args: '', description: 'Leave the session', noKey: true },
433
+ // REPL-only meta-commands (handled in repl.ts, not in Commander).
434
+ // Marked replOnly so the CI sync guard skips them.
435
+ { name: 'agents', args: '', description: 'List installed specialists', noKey: true, replOnly: true },
436
+ { name: 'history', args: '', description: 'Recent REPL session history', noKey: true, replOnly: true },
437
+ { name: 'clear', args: '', description: 'Clear the screen', noKey: true, replOnly: true },
438
+ { name: 'exit', args: '', description: 'Leave the session', noKey: true, replOnly: true },
431
439
  ];
432
440
  export function showSlashCommandMenu() {
433
441
  newline();
@@ -64,7 +64,7 @@ function checkTypoSingle(token, knownCommands) {
64
64
  .map((n) => ({ n, d: levenshtein(lower, n) }))
65
65
  .filter((s) => s.d > 0 && s.d <= 2 && s.d < lower.length)
66
66
  // Deterministic tie-breaker (alphabetical) so ranking is stable across runs.
67
- .sort((a, b) => (a.d - b.d) || a.n.localeCompare(b.n))
67
+ .sort((a, b) => a.d - b.d || a.n.localeCompare(b.n))
68
68
  .slice(0, 3);
69
69
  return scored.length > 0 ? scored.map((s) => s.n) : null;
70
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "build": "tsc",
22
22
  "postbuild": "chmod +x dist/index.js",
23
23
  "repomap": "node scripts/gen-repo-map.mjs",
24
+ "pattern-counts": "node scripts/print-pattern-counts.mjs",
24
25
  "prepublishOnly": "npm run build && find dist -name '*.map' -delete && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete",
25
26
  "start": "node dist/index.js",
26
27
  "dev": "tsx src/index.ts",