codehere 0.6.1 → 0.7.0
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 +5 -3
- package/dist/application/use-cases/domain-trust.js +110 -0
- package/dist/application/use-cases/generate-final-report.js +84 -5
- package/dist/application/use-cases/generate-narration.js +20 -3
- package/dist/application/use-cases/generate-thread-title.js +4 -0
- package/dist/application/use-cases/thread-export.js +155 -0
- package/dist/application/use-cases/verify-claim.js +274 -0
- package/dist/application/use-cases/write-pass.js +277 -0
- package/dist/index.js +218 -19
- package/dist/infrastructure/completion/bash-completion.js +2 -2
- package/dist/infrastructure/config/config-manager.js +1 -1
- package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
- package/dist/infrastructure/context/adapters/figma-source.js +163 -0
- package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
- package/dist/infrastructure/context/adapters/linear-source.js +190 -0
- package/dist/infrastructure/context/context-resolver.js +144 -0
- package/dist/infrastructure/context/context-source.js +114 -0
- package/dist/infrastructure/context/oauth-flow.js +207 -0
- package/dist/infrastructure/context/oauth-store.js +119 -0
- package/dist/infrastructure/context/registry-bootstrap.js +99 -0
- package/dist/infrastructure/context/write-pass-resolver.js +106 -0
- 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 +79 -59
- package/dist/infrastructure/hooks/session-analyzer.js +65 -8
- package/dist/infrastructure/observability/execution-tracer.js +0 -5
- package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
- package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
- package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
- package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
- package/dist/infrastructure/orchestration/context-handoff.js +4 -3
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
- package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
- package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
- package/dist/infrastructure/orchestration/session-manager.js +142 -29
- package/dist/infrastructure/orchestration/session-persistence.js +139 -30
- package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
- package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
- package/dist/infrastructure/protocols/cloud-sync.js +10 -11
- package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
- package/dist/infrastructure/protocols/thread-phase.js +105 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
- package/dist/infrastructure/protocols/ui-page.js +12 -3299
- package/dist/infrastructure/protocols/verify-page.js +941 -0
- package/dist/infrastructure/security/trust-scorer.js +76 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
- package/dist/infrastructure/storage/project-thread-migration.js +14 -9
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
- package/dist/monitoring.js +1 -1
- package/dist/presentation/cli/commands/audit-command.js +126 -0
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/config-command.js +2 -2
- package/dist/presentation/cli/commands/context-source-command.js +198 -0
- package/dist/presentation/cli/commands/help-command.js +51 -1
- 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/commands/report-command.js +6 -4
- package/dist/presentation/cli/commands/setup-command.js +71 -18
- package/dist/presentation/cli/commands/status-command.js +0 -4
- package/dist/presentation/cli/commands/thread-command.js +288 -0
- package/dist/presentation/cli/commands/verify-command.js +233 -0
- package/dist/presentation/cli/error-display.js +1 -1
- package/dist/presentation/cli/repl.js +52 -2
- package/dist/presentation/cli/task-deliverable.js +127 -0
- package/dist/testing/cassette.js +125 -0
- package/dist/testing/replay-adapter.js +166 -0
- package/dist/ui.js +54 -29
- package/dist/utils/typo-guard.js +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -39,6 +39,10 @@ import { executeNarratorCommand } from './presentation/cli/commands/narrator-com
|
|
|
39
39
|
import { executeTrustCommand } from './presentation/cli/commands/trust-command.js';
|
|
40
40
|
import { executeReportCommand } from './presentation/cli/commands/report-command.js';
|
|
41
41
|
import { executeScanToolCommand } from './presentation/cli/commands/scan-tool-command.js';
|
|
42
|
+
import { executeVerifyCommand } from './presentation/cli/commands/verify-command.js';
|
|
43
|
+
import { executeAuditCommand } from './presentation/cli/commands/audit-command.js';
|
|
44
|
+
import { executeContextSourceCommand } from './presentation/cli/commands/context-source-command.js';
|
|
45
|
+
import { executeThreadCommand } from './presentation/cli/commands/thread-command.js';
|
|
42
46
|
import { displayError } from './presentation/cli/error-display.js';
|
|
43
47
|
import { performHealthCheck, formatHealthStatus, getMetrics } from './monitoring.js';
|
|
44
48
|
import { colors, createBanner, createTable, createWelcomeMessage, success, error, info, sectionHeader, formatNumber, newline, isMinimalStyle, printMinimalHeader, } from './ui.js';
|
|
@@ -58,7 +62,7 @@ import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
|
|
|
58
62
|
const program = new Command();
|
|
59
63
|
program
|
|
60
64
|
.name('codehere')
|
|
61
|
-
.description('A PM for your AI coding agents. Delegate a task, watch it run, get a plain-English report with a trust score on every file.')
|
|
65
|
+
.description('A PM for your AI coding agents. Delegate a task, watch it run, and get a plain-English report with a trust score on every file it changed.')
|
|
62
66
|
.version(getVersion())
|
|
63
67
|
.configureHelp({ helpWidth: 80, sortSubcommands: true })
|
|
64
68
|
// rc.18: many users hit `codehere --help` first and get Commander's flat
|
|
@@ -77,7 +81,7 @@ program
|
|
|
77
81
|
// typo guard miss the unknown-command shape. See typo-guard.ts.
|
|
78
82
|
.argument('[query...]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
|
|
79
83
|
.action(async (queryParts) => {
|
|
80
|
-
const joined =
|
|
84
|
+
const joined = queryParts && queryParts.length > 0 ? queryParts.join(' ') : '';
|
|
81
85
|
// One-shot delegate: `codehere "review src/auth.ts"` runs that task and exits.
|
|
82
86
|
// Useful for scripting or quick one-offs without entering the REPL.
|
|
83
87
|
if (joined && joined.trim()) {
|
|
@@ -118,6 +122,10 @@ program
|
|
|
118
122
|
}
|
|
119
123
|
if (!process.stdin.isTTY) {
|
|
120
124
|
console.log(createWelcomeMessage());
|
|
125
|
+
// Non-interactive (piped / scripted): the REPL can't run, so leave the
|
|
126
|
+
// user with ONE clear next step instead of a dead-end welcome screen.
|
|
127
|
+
console.log(colors.dim(' Next: ') + colors.cyan('codehere start') + colors.dim(' open the dashboard'));
|
|
128
|
+
console.log(colors.dim(' New here? ') + colors.cyan('codehere setup') + colors.dim(' one-time setup\n'));
|
|
121
129
|
return;
|
|
122
130
|
}
|
|
123
131
|
const { runInteractiveRepl } = await import('./presentation/cli/repl.js');
|
|
@@ -354,6 +362,140 @@ Examples:
|
|
|
354
362
|
process.exit(1);
|
|
355
363
|
}
|
|
356
364
|
});
|
|
365
|
+
// ── verify ───────────────────────────────────────────────────────────────────
|
|
366
|
+
// v0.7 verifier-agent (F-03). Catches the easy lies a Final Report makes
|
|
367
|
+
// against the structured artifacts recorded during the session.
|
|
368
|
+
program
|
|
369
|
+
.command('verify')
|
|
370
|
+
.description("Verify a session's Final Report against the recorded reality (catches optimistic claims)")
|
|
371
|
+
.argument('[session-id]', 'Session id (or short prefix) to verify. Omit to list recent sessions.')
|
|
372
|
+
.option('--json', 'Output machine-readable JSON instead of formatted findings')
|
|
373
|
+
.addHelpText('after', `
|
|
374
|
+
Examples:
|
|
375
|
+
$ codehere verify # list recent sessions to pick from
|
|
376
|
+
$ codehere verify a1b2c3d4 # verify by short id prefix
|
|
377
|
+
$ codehere verify a1b2c3d4 --json # machine-readable verdict for CI
|
|
378
|
+
|
|
379
|
+
Exit codes:
|
|
380
|
+
0 = verified, partial (warnings only), or no Final Report to verify
|
|
381
|
+
1 = broken (a claim contradicts recorded reality)
|
|
382
|
+
2 = session not found
|
|
383
|
+
`)
|
|
384
|
+
.action(async (sessionId, options) => {
|
|
385
|
+
try {
|
|
386
|
+
// Per codex Round 2: set exitCode instead of calling process.exit so a
|
|
387
|
+
// piped --json consumer sees the full payload before Node tears down.
|
|
388
|
+
// The picker mode can write ~15 sessions worth of JSON · enough to
|
|
389
|
+
// exceed pipe buffer + race the process exit. exitCode preserves the
|
|
390
|
+
// intent (CI uses the exit code) without forcing an immediate teardown.
|
|
391
|
+
process.exitCode = await executeVerifyCommand(sessionId, { json: options.json });
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// ── audit ────────────────────────────────────────────────────────────────────
|
|
399
|
+
// F-05 of the demo v12 backlog · expose the hash-chain verifier as a CLI.
|
|
400
|
+
program
|
|
401
|
+
.command('audit')
|
|
402
|
+
.description('Inspect the hash-chained audit log · verify the chain or show one entry')
|
|
403
|
+
.argument('[subcommand]', 'verify | show', 'verify')
|
|
404
|
+
.argument('[arg]', 'For `show`: the sequence number to print.')
|
|
405
|
+
.option('--json', 'Output machine-readable JSON instead of formatted text')
|
|
406
|
+
.addHelpText('after', `
|
|
407
|
+
Examples:
|
|
408
|
+
$ codehere audit # equivalent to: codehere audit verify
|
|
409
|
+
$ codehere audit verify # walk the chain, report intact/tampered
|
|
410
|
+
$ codehere audit verify --json # machine-readable verdict for CI
|
|
411
|
+
$ codehere audit show 42 # print entry with seq=42
|
|
412
|
+
$ codehere audit show 42 --json
|
|
413
|
+
|
|
414
|
+
Exit codes:
|
|
415
|
+
0 = chain intact OR entry found
|
|
416
|
+
1 = chain BROKEN (one or more break points detected)
|
|
417
|
+
2 = invalid input / no entry found
|
|
418
|
+
`)
|
|
419
|
+
.action((sub, arg, options) => {
|
|
420
|
+
try {
|
|
421
|
+
process.exitCode = executeAuditCommand(sub, arg, { json: options.json });
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
425
|
+
process.exitCode = 1;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
// ── context-source ───────────────────────────────────────────────────────────
|
|
429
|
+
// v0.8 S2.5 · OAuth click-to-connect for Linear, Figma, etc.
|
|
430
|
+
program
|
|
431
|
+
.command('context-source')
|
|
432
|
+
.alias('cs')
|
|
433
|
+
.description('Manage context sources (Linear, Figma) · connect via OAuth, list status, disconnect')
|
|
434
|
+
.argument('[subcommand]', 'list | connect | disconnect', 'list')
|
|
435
|
+
.argument('[provider]', 'For connect/disconnect: linear | figma | ...')
|
|
436
|
+
.option('--json', 'Machine-readable output')
|
|
437
|
+
.option('--no-open', 'For connect: skip auto-opening the browser (just print the URL)')
|
|
438
|
+
.addHelpText('after', `
|
|
439
|
+
Examples:
|
|
440
|
+
$ codehere context-source # equivalent to: list
|
|
441
|
+
$ codehere context-source list
|
|
442
|
+
$ codehere context-source connect linear
|
|
443
|
+
$ codehere context-source connect figma --no-open
|
|
444
|
+
$ codehere context-source disconnect linear
|
|
445
|
+
|
|
446
|
+
Tokens persist at ~/.codehere/context-sources.json (0600 mode).
|
|
447
|
+
Requires the daemon running: codehere start
|
|
448
|
+
Provider client IDs come from env: CODEHERE_LINEAR_CLIENT_ID, CODEHERE_FIGMA_CLIENT_ID
|
|
449
|
+
`)
|
|
450
|
+
.action(async (sub, provider, options) => {
|
|
451
|
+
try {
|
|
452
|
+
process.exitCode = await executeContextSourceCommand(sub, provider, {
|
|
453
|
+
json: options.json,
|
|
454
|
+
open: options.open,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
459
|
+
process.exitCode = 1;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// ── thread ───────────────────────────────────────────────────────────────────
|
|
463
|
+
// v0.8 S4 · signed-payload "GitHub for agent sessions" · share/import threads
|
|
464
|
+
program
|
|
465
|
+
.command('thread')
|
|
466
|
+
.description('Share or import a session · Ed25519-signed bundle · local-first')
|
|
467
|
+
.argument('<subcommand>', 'export | import | keys | trust | untrust')
|
|
468
|
+
.argument('[arg]', 'For export: session-id · For import/trust/untrust: file path or pubkey')
|
|
469
|
+
.argument('[label]', 'For trust: an optional human label for the key')
|
|
470
|
+
.option('--out <file>', 'For export: write the signed bundle to a file (default: stdout)')
|
|
471
|
+
.option('--trust', 'For import: trust the signer key automatically if unknown')
|
|
472
|
+
.option('--json', 'Machine-readable output')
|
|
473
|
+
.addHelpText('after', `
|
|
474
|
+
Examples:
|
|
475
|
+
$ codehere thread export a1b2c3d4 --out demo-thread.json
|
|
476
|
+
$ codehere thread import ./shared.json --trust
|
|
477
|
+
$ codehere thread keys
|
|
478
|
+
$ codehere thread trust <pubkey-base64> "Daniel"
|
|
479
|
+
$ codehere thread untrust <pubkey-base64>
|
|
480
|
+
|
|
481
|
+
Bundles include: spawned · steps · artifacts · finalReport · signature.
|
|
482
|
+
No source code · only the synthesized record.
|
|
483
|
+
Recipient must trust the signer key (or pass --trust) before replay.
|
|
484
|
+
`)
|
|
485
|
+
.action((sub, arg, label, options) => {
|
|
486
|
+
try {
|
|
487
|
+
process.exitCode = executeThreadCommand(sub, arg, label, {
|
|
488
|
+
out: options.out,
|
|
489
|
+
trust: options.trust,
|
|
490
|
+
label,
|
|
491
|
+
json: options.json,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
496
|
+
process.exitCode = 1;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
357
499
|
// ── hook ─────────────────────────────────────────────────────────────────────
|
|
358
500
|
program
|
|
359
501
|
.command('hook')
|
|
@@ -581,13 +723,17 @@ program
|
|
|
581
723
|
const { SessionManager } = require('./infrastructure/orchestration/session-manager.js');
|
|
582
724
|
SessionManager.getInstance().shutdown();
|
|
583
725
|
}
|
|
584
|
-
catch {
|
|
726
|
+
catch {
|
|
727
|
+
/* graceful degradation — daemon may not have started fully */
|
|
728
|
+
}
|
|
585
729
|
if (cloud) {
|
|
586
730
|
try {
|
|
587
731
|
const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
|
|
588
732
|
stopCloudSync();
|
|
589
733
|
}
|
|
590
|
-
catch {
|
|
734
|
+
catch {
|
|
735
|
+
/* cloud sync may not be loaded */
|
|
736
|
+
}
|
|
591
737
|
}
|
|
592
738
|
server.close();
|
|
593
739
|
};
|
|
@@ -634,10 +780,18 @@ program
|
|
|
634
780
|
// ── task (spawn an agent session via CLI) ────────────────────────────────────
|
|
635
781
|
program
|
|
636
782
|
.command('task <prompt...>')
|
|
637
|
-
.description('Assign a task — spawns
|
|
783
|
+
.description('Assign a task — spawns an agent under codehere. Requires `codehere start` running.')
|
|
638
784
|
.option('-p, --port <number>', 'Daemon port', '3773')
|
|
639
785
|
.option('--host <host>', 'Daemon host', '127.0.0.1')
|
|
640
|
-
.option('--
|
|
786
|
+
.option('--agent <id>', 'Which agent to run (claude-code, codex, aider, gemini, opencode). Default: auto.')
|
|
787
|
+
.option('--watch', 'Watch it work and print the report when it finishes')
|
|
788
|
+
.addHelpText('after', `
|
|
789
|
+
Examples:
|
|
790
|
+
$ codehere task "fix the totalValue bug in inventory.js" # assign, get a session id
|
|
791
|
+
$ codehere task "add a health check route" --watch # watch it work + see the report
|
|
792
|
+
$ codehere task "summarize recent changes" --agent codex --watch # pick the agent
|
|
793
|
+
|
|
794
|
+
Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`).`)
|
|
641
795
|
.action(async (promptParts, options) => {
|
|
642
796
|
const prompt = promptParts.join(' ').trim();
|
|
643
797
|
if (!prompt) {
|
|
@@ -648,8 +802,9 @@ program
|
|
|
648
802
|
const host = options?.host ?? '127.0.0.1';
|
|
649
803
|
const base = `http://${host}:${port}`;
|
|
650
804
|
const http = await import('http');
|
|
805
|
+
const { waitForDeliverable, formatDeliverable } = await import('./presentation/cli/task-deliverable.js');
|
|
651
806
|
const spawnResult = await new Promise((resolve) => {
|
|
652
|
-
const body = JSON.stringify({ prompt });
|
|
807
|
+
const body = JSON.stringify({ prompt, ...(options?.agent ? { agent: options.agent } : {}) });
|
|
653
808
|
const r = http.request(`${base}/api/task`, {
|
|
654
809
|
method: 'POST',
|
|
655
810
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
@@ -672,16 +827,26 @@ program
|
|
|
672
827
|
r.end();
|
|
673
828
|
});
|
|
674
829
|
if (!spawnResult || !spawnResult.sessionId) {
|
|
675
|
-
|
|
830
|
+
if (spawnResult?.error)
|
|
831
|
+
console.error(error(spawnResult.error));
|
|
832
|
+
else
|
|
833
|
+
console.error(error(`Could not reach the daemon at ${base}. Start it with: codehere start`));
|
|
676
834
|
process.exit(1);
|
|
677
835
|
}
|
|
678
|
-
|
|
679
|
-
console.log(
|
|
680
|
-
if (!options?.watch)
|
|
836
|
+
const sid = spawnResult.sessionId;
|
|
837
|
+
console.log(success(`Task assigned · session ${sid.slice(0, 8)}`));
|
|
838
|
+
if (!options?.watch) {
|
|
839
|
+
console.log(colors.dim(` Watch in the browser: ${base}/`));
|
|
840
|
+
console.log(colors.dim(` Or in the terminal: codehere task "${prompt}" --watch`));
|
|
841
|
+
console.log(colors.dim(` Stop it: codehere kill ${sid.slice(0, 8)}`));
|
|
681
842
|
return;
|
|
682
|
-
|
|
843
|
+
}
|
|
844
|
+
// --watch · render the agent's actual steps as they complete, then print
|
|
845
|
+
// the deliverable (report + files + trust + verifier) when it's done.
|
|
846
|
+
console.log(colors.dim(` Watching… (Ctrl-C to stop watching; the agent keeps running)`));
|
|
847
|
+
const printed = new Set();
|
|
683
848
|
await new Promise((resolve) => {
|
|
684
|
-
const r = http.get(
|
|
849
|
+
const r = http.get(`${base}/api/task/${sid}/events`, (res) => {
|
|
685
850
|
let buf = '';
|
|
686
851
|
res.on('data', (c) => {
|
|
687
852
|
buf += c.toString();
|
|
@@ -691,20 +856,37 @@ program
|
|
|
691
856
|
buf = buf.slice(i + 2);
|
|
692
857
|
if (!frame.startsWith('data:'))
|
|
693
858
|
continue;
|
|
694
|
-
const payload = frame.slice(5).trim();
|
|
695
859
|
try {
|
|
696
|
-
const event = JSON.parse(
|
|
860
|
+
const event = JSON.parse(frame.slice(5).trim());
|
|
697
861
|
const type = String(event.type || 'message');
|
|
698
|
-
if (type === '
|
|
699
|
-
|
|
862
|
+
if (type === 'codehere:steps') {
|
|
863
|
+
const steps = (event.data?.steps ?? []);
|
|
864
|
+
for (const s of steps) {
|
|
865
|
+
if (s.state !== 'done' && s.state !== 'failed')
|
|
866
|
+
continue;
|
|
867
|
+
if (printed.has(s.label))
|
|
868
|
+
continue; // one line per step, on first settle
|
|
869
|
+
printed.add(s.label);
|
|
870
|
+
const mark = s.state === 'failed' ? colors.error('✗') : colors.success('✓');
|
|
871
|
+
const text = s.kind === 'say' ? colors.dim(s.label) : s.label;
|
|
872
|
+
console.log(` ${mark} ${text}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
else if (type === 'codehere:error') {
|
|
876
|
+
const d = event.data;
|
|
877
|
+
if (d?.message)
|
|
878
|
+
console.log(` ${colors.error('!')} ${d.message}`);
|
|
879
|
+
if (d?.recovery)
|
|
880
|
+
console.log(colors.dim(` ${d.recovery}`));
|
|
881
|
+
}
|
|
882
|
+
else if (type === 'exit') {
|
|
700
883
|
res.destroy();
|
|
701
884
|
resolve();
|
|
702
885
|
return;
|
|
703
886
|
}
|
|
704
|
-
console.log(colors.dim(` [${type}]`));
|
|
705
887
|
}
|
|
706
888
|
catch {
|
|
707
|
-
/* skip */
|
|
889
|
+
/* skip malformed frame */
|
|
708
890
|
}
|
|
709
891
|
}
|
|
710
892
|
});
|
|
@@ -712,6 +894,23 @@ program
|
|
|
712
894
|
});
|
|
713
895
|
r.on('error', () => resolve());
|
|
714
896
|
});
|
|
897
|
+
// The Final Report is generated after exit — poll for it, then print.
|
|
898
|
+
// Real agents take ~30-60s to write it, so tell the user we're waiting.
|
|
899
|
+
process.stdout.write(colors.dim(' Writing the report…'));
|
|
900
|
+
const result = await waitForDeliverable(base, sid);
|
|
901
|
+
process.stdout.write('\r' + ' '.repeat(24) + '\r'); // clear the waiting line
|
|
902
|
+
if (result && (result.finalReport || result.failedReport || result.status !== 'completed')) {
|
|
903
|
+
console.log(formatDeliverable(result));
|
|
904
|
+
}
|
|
905
|
+
else if (result) {
|
|
906
|
+
// Completed, but the report is still generating past our wait window.
|
|
907
|
+
console.log(formatDeliverable(result));
|
|
908
|
+
console.log(colors.dim(' (the written report is still being generated — run the command below in a moment)'));
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
console.log(colors.dim('\n Task finished, but the report could not be fetched from the daemon.'));
|
|
912
|
+
}
|
|
913
|
+
console.log(colors.dim(`Next: codehere verify ${sid.slice(0, 8)} (see this report + the fact-check anytime)`));
|
|
715
914
|
});
|
|
716
915
|
// ── kill (stop a running task) ───────────────────────────────────────────────
|
|
717
916
|
program
|
|
@@ -13,9 +13,9 @@ _complete_codehere() {
|
|
|
13
13
|
opts="ask plan orchestrate fix suggest run review status help version"
|
|
14
14
|
|
|
15
15
|
if [[ \${cur} == -* ]]; then
|
|
16
|
-
COMPREPLY=(
|
|
16
|
+
COMPREPLY=($(compgen -W "--help --version" -- \${cur}))
|
|
17
17
|
else
|
|
18
|
-
COMPREPLY=(
|
|
18
|
+
COMPREPLY=($(compgen -W "\${opts}" -- \${cur}))
|
|
19
19
|
fi
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -103,7 +103,7 @@ export class ConfigManager {
|
|
|
103
103
|
writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
104
104
|
}
|
|
105
105
|
catch (error) {
|
|
106
|
-
throw new Error(`Failed to save config to ${this.configPath}: ${error instanceof Error ? error.message : String(error)}
|
|
106
|
+
throw new Error(`Failed to save config to ${this.configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draw.io context-source adapter (v0.8 S5 · third reference adapter).
|
|
3
|
+
*
|
|
4
|
+
* URL shapes accepted (we route both via the same scheme to keep the
|
|
5
|
+
* agent prompt forgiving):
|
|
6
|
+
* - `drawio://<gist-or-raw-url-base64>` · base64url-encoded source URL
|
|
7
|
+
* - `drawio://gh/<owner>/<repo>/<path>` · raw GitHub-hosted .drawio XML
|
|
8
|
+
* - `drawio://url/<urlencoded-https-url>` · arbitrary HTTPS-hosted .drawio
|
|
9
|
+
*
|
|
10
|
+
* No OAuth · draw.io is a client-side editor, the file itself is the
|
|
11
|
+
* payload. We fetch the raw XML, summarize node + edge counts, and emit
|
|
12
|
+
* the first ~3KB of the XML so the agent can read structure without us
|
|
13
|
+
* doing graphviz-style parsing here. Bigger diagrams get a `...truncated`
|
|
14
|
+
* suffix; agents can ask the human for a screenshot if they need pixels.
|
|
15
|
+
*
|
|
16
|
+
* Local-first: no codehere endpoint is contacted. Network call goes
|
|
17
|
+
* directly to GitHub-raw / the supplied HTTPS host. Audit chain records
|
|
18
|
+
* fetch + byte count, never content.
|
|
19
|
+
*/
|
|
20
|
+
import { ContextSourceError, estimateTokens, } from '../context-source.js';
|
|
21
|
+
const MAX_XML_CHARS = 3000;
|
|
22
|
+
const MAX_RESPONSE_BYTES = 1_500_000; // 1.5MB · refuse anything bigger to protect prompt budget
|
|
23
|
+
/** Turn the codehere drawio:// URL into a concrete HTTPS fetch target. */
|
|
24
|
+
function resolveTarget(url) {
|
|
25
|
+
const raw = url.toString();
|
|
26
|
+
const after = raw.slice('drawio://'.length).replace(/^\/+/, '');
|
|
27
|
+
if (!after) {
|
|
28
|
+
throw new ContextSourceError('drawio', 'invalid_url', 'drawio URL missing path');
|
|
29
|
+
}
|
|
30
|
+
// gh/<owner>/<repo>/<path...>
|
|
31
|
+
if (after.startsWith('gh/')) {
|
|
32
|
+
const parts = after.slice('gh/'.length).split('/');
|
|
33
|
+
if (parts.length < 3) {
|
|
34
|
+
throw new ContextSourceError('drawio', 'invalid_url', 'gh/ path needs <owner>/<repo>/<path>');
|
|
35
|
+
}
|
|
36
|
+
const [owner, repo, ...rest] = parts;
|
|
37
|
+
const path = rest.join('/');
|
|
38
|
+
if (!/^[A-Za-z0-9._-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(repo)) {
|
|
39
|
+
throw new ContextSourceError('drawio', 'invalid_url', 'gh/ owner/repo must be alphanumeric');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
fetchUrl: `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${path}`,
|
|
43
|
+
label: `github.com/${owner}/${repo}/${path}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// url/<urlencoded-https-url>
|
|
47
|
+
if (after.startsWith('url/')) {
|
|
48
|
+
const encoded = after.slice('url/'.length);
|
|
49
|
+
const decoded = safeDecode(encoded);
|
|
50
|
+
if (!/^https:\/\//.test(decoded)) {
|
|
51
|
+
throw new ContextSourceError('drawio', 'invalid_url', 'url/ target must be https://');
|
|
52
|
+
}
|
|
53
|
+
return { fetchUrl: decoded, label: decoded };
|
|
54
|
+
}
|
|
55
|
+
// base64url-encoded full URL
|
|
56
|
+
const decoded = safeBase64UrlDecode(after);
|
|
57
|
+
if (decoded && /^https:\/\//.test(decoded)) {
|
|
58
|
+
return { fetchUrl: decoded, label: decoded };
|
|
59
|
+
}
|
|
60
|
+
throw new ContextSourceError('drawio', 'invalid_url', 'use drawio://gh/<owner>/<repo>/<path> · drawio://url/<urlencoded-https> · or drawio://<base64url-https>');
|
|
61
|
+
}
|
|
62
|
+
function safeDecode(s) {
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent(s);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function safeBase64UrlDecode(s) {
|
|
71
|
+
try {
|
|
72
|
+
const pad = '='.repeat((4 - (s.length % 4)) % 4);
|
|
73
|
+
return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64').toString('utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export const drawioSource = {
|
|
80
|
+
id: 'drawio',
|
|
81
|
+
label: 'draw.io',
|
|
82
|
+
scheme: 'drawio',
|
|
83
|
+
description: 'Fetch a draw.io / diagrams.net diagram XML (architecture, flowchart, etc.)',
|
|
84
|
+
isConfigured() {
|
|
85
|
+
// Public-by-URL · no auth needed. Always available.
|
|
86
|
+
return true;
|
|
87
|
+
},
|
|
88
|
+
async fetch(url, ctx) {
|
|
89
|
+
const target = resolveTarget(url);
|
|
90
|
+
const res = await ctx.httpGet(target.fetchUrl, { 'User-Agent': 'codehere/0.8' });
|
|
91
|
+
if (res.status === 404) {
|
|
92
|
+
throw new ContextSourceError('drawio', 'not_found', `diagram not found at ${target.label}`);
|
|
93
|
+
}
|
|
94
|
+
if (res.status === 429) {
|
|
95
|
+
throw new ContextSourceError('drawio', 'rate_limit', `host returned 429 for ${target.label}`);
|
|
96
|
+
}
|
|
97
|
+
if (res.status !== 200) {
|
|
98
|
+
throw new ContextSourceError('drawio', 'network', `host returned ${res.status} for ${target.label}: ${res.body.slice(0, 200)}`);
|
|
99
|
+
}
|
|
100
|
+
if (res.body.length > MAX_RESPONSE_BYTES) {
|
|
101
|
+
throw new ContextSourceError('drawio', 'parse', `diagram too big (${res.body.length} bytes · limit ${MAX_RESPONSE_BYTES})`);
|
|
102
|
+
}
|
|
103
|
+
if (!looksLikeDrawio(res.body)) {
|
|
104
|
+
throw new ContextSourceError('drawio', 'parse', 'response does not look like a draw.io XML or SVG file');
|
|
105
|
+
}
|
|
106
|
+
const stats = quickStats(res.body);
|
|
107
|
+
const text = renderDiagramAsPlainText(target, res.body, stats);
|
|
108
|
+
return {
|
|
109
|
+
url: url.toString(),
|
|
110
|
+
source: 'drawio',
|
|
111
|
+
fetchedAt: new Date(ctx.now ? ctx.now() : Date.now()).toISOString(),
|
|
112
|
+
text,
|
|
113
|
+
estimatedTokens: estimateTokens(text),
|
|
114
|
+
metadata: {
|
|
115
|
+
label: target.label,
|
|
116
|
+
bytes: res.body.length,
|
|
117
|
+
cellCount: stats.cellCount,
|
|
118
|
+
edgeCount: stats.edgeCount,
|
|
119
|
+
pageCount: stats.pageCount,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
/** Does this payload look like a draw.io diagram (XML) or SVG export?
|
|
125
|
+
* Accept any prolog (whitespace, BOM, `<?xml ...?>`) before the first
|
|
126
|
+
* meaningful tag · `<mxfile`, `<mxGraphModel`, or `<svg`. */
|
|
127
|
+
function looksLikeDrawio(body) {
|
|
128
|
+
const trimmed = body.replace(/^\uFEFF/, '').trimStart();
|
|
129
|
+
const stripped = trimmed.startsWith('<?') ? trimmed.slice(trimmed.indexOf('?>') + 2).trimStart() : trimmed;
|
|
130
|
+
return /^<(mxfile|mxGraphModel|svg)\b/i.test(stripped);
|
|
131
|
+
}
|
|
132
|
+
/** Cheap regex stats · we don't ship a real XML parser. */
|
|
133
|
+
function quickStats(xml) {
|
|
134
|
+
const cellMatches = xml.match(/<mxCell\b/g);
|
|
135
|
+
const edgeMatches = xml.match(/<mxCell\b[^>]*\bedge="1"/g);
|
|
136
|
+
const pageMatches = xml.match(/<diagram\b/g);
|
|
137
|
+
return {
|
|
138
|
+
cellCount: cellMatches ? cellMatches.length : 0,
|
|
139
|
+
edgeCount: edgeMatches ? edgeMatches.length : 0,
|
|
140
|
+
pageCount: pageMatches ? pageMatches.length : 1,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function renderDiagramAsPlainText(target, xml, stats) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`draw.io diagram · ${target.label}`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(`structure: ${stats.cellCount} cells · ${stats.edgeCount} edges · ${stats.pageCount} page${stats.pageCount === 1 ? '' : 's'}`);
|
|
148
|
+
lines.push(`bytes: ${xml.length}`);
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push('--- raw xml (truncated) ---');
|
|
151
|
+
lines.push(xml.slice(0, MAX_XML_CHARS));
|
|
152
|
+
if (xml.length > MAX_XML_CHARS) {
|
|
153
|
+
lines.push(`...[truncated · diagram is ${xml.length} chars · ask the human for a screenshot if structure isn't clear]`);
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=drawio-source.js.map
|