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.
- package/README.md +8 -8
- package/dist/application/use-cases/generate-final-report.js +57 -2
- package/dist/application/use-cases/generate-narration.js +9 -3
- package/dist/index.js +34 -4
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +5 -5
- package/dist/infrastructure/hooks/claude-sdk-hook.js +48 -2
- package/dist/infrastructure/orchestration/codex-adapter.js +11 -6
- package/dist/infrastructure/orchestration/context-handoff.js +1 -1
- package/dist/infrastructure/orchestration/gemini-adapter.js +3 -15
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/opencode-adapter.js +42 -1
- package/dist/infrastructure/orchestration/progress-aggregator.js +1 -2
- package/dist/infrastructure/orchestration/session-manager.js +178 -76
- package/dist/infrastructure/protocols/cloud-sync.js +9 -5
- package/dist/infrastructure/protocols/mcp-server-impl.js +368 -334
- package/dist/infrastructure/storage/cross-tool-agents.js +19 -2
- package/dist/infrastructure/storage/project-thread-migration.js +12 -7
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +6 -2
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/init-command.js +5 -4
- package/dist/presentation/cli/commands/memory-command.js +29 -15
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
- package/dist/presentation/cli/repl.js +20 -1
- package/dist/ui.js +36 -28
- package/dist/utils/typo-guard.js +1 -1
- 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
|
|
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([
|
|
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 [
|
|
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 [
|
|
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 {
|
|
181
|
+
catch {
|
|
182
|
+
/* skip malformed */
|
|
183
|
+
}
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
catch {
|
|
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(
|
|
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(
|
|
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') +
|
|
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
|
-
?
|
|
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({
|
|
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({
|
|
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({
|
|
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')
|
|
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
|
-
|
|
126
|
-
|
|
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)
|
|
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
|
-
//
|
|
402
|
-
{ name: '
|
|
403
|
-
{ name: '
|
|
404
|
-
{ name: '
|
|
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: '
|
|
425
|
-
{ name: '
|
|
426
|
-
{ name: '
|
|
427
|
-
//
|
|
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
|
-
|
|
430
|
-
|
|
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();
|
package/dist/utils/typo-guard.js
CHANGED
|
@@ -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) =>
|
|
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.
|
|
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",
|