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.
Files changed (81) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +84 -5
  4. package/dist/application/use-cases/generate-narration.js +20 -3
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +218 -19
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/cost/budget-config.js +1 -1
  23. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  24. package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
  25. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  26. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  27. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  28. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  29. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  30. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  31. package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
  32. package/dist/infrastructure/orchestration/context-handoff.js +4 -3
  33. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  34. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  35. package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
  36. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  37. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  38. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  40. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  41. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  42. package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
  43. package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
  44. package/dist/infrastructure/orchestration/session-manager.js +142 -29
  45. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  46. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  47. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  48. package/dist/infrastructure/protocols/cloud-sync.js +10 -11
  49. package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
  50. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  51. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  52. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  53. package/dist/infrastructure/protocols/verify-page.js +941 -0
  54. package/dist/infrastructure/security/trust-scorer.js +76 -1
  55. package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
  56. package/dist/infrastructure/storage/project-thread-migration.js +14 -9
  57. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  58. package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
  59. package/dist/monitoring.js +1 -1
  60. package/dist/presentation/cli/commands/audit-command.js +126 -0
  61. package/dist/presentation/cli/commands/budget-command.js +2 -2
  62. package/dist/presentation/cli/commands/config-command.js +2 -2
  63. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  64. package/dist/presentation/cli/commands/help-command.js +51 -1
  65. package/dist/presentation/cli/commands/init-command.js +5 -4
  66. package/dist/presentation/cli/commands/memory-command.js +29 -15
  67. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  68. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  69. package/dist/presentation/cli/commands/report-command.js +6 -4
  70. package/dist/presentation/cli/commands/setup-command.js +71 -18
  71. package/dist/presentation/cli/commands/status-command.js +0 -4
  72. package/dist/presentation/cli/commands/thread-command.js +288 -0
  73. package/dist/presentation/cli/commands/verify-command.js +233 -0
  74. package/dist/presentation/cli/error-display.js +1 -1
  75. package/dist/presentation/cli/repl.js +52 -2
  76. package/dist/presentation/cli/task-deliverable.js +127 -0
  77. package/dist/testing/cassette.js +125 -0
  78. package/dist/testing/replay-adapter.js +166 -0
  79. package/dist/ui.js +54 -29
  80. package/dist/utils/typo-guard.js +1 -1
  81. 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 = (queryParts && queryParts.length > 0) ? queryParts.join(' ') : '';
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 { /* graceful degradation — daemon may not have started fully */ }
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 { /* cloud sync may not be loaded */ }
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 the default agent under codehere. Requires `codehere start` running.')
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('--watch', 'Stream events to the terminal (default: just returns the session id)')
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
- console.error(error(`Could not reach daemon at ${base}. Start it with: codehere start`));
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
- console.log(success(`Task assigned · session ${spawnResult.sessionId}`));
679
- console.log(colors.dim(` Watch: ${base}/ · Kill: codehere kill ${spawnResult.sessionId}`));
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
- const streamUrl = `${base}/api/task/${spawnResult.sessionId}/events`;
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(streamUrl, (res) => {
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(payload);
860
+ const event = JSON.parse(frame.slice(5).trim());
697
861
  const type = String(event.type || 'message');
698
- if (type === 'exit') {
699
- console.log(colors.dim(` [exit] ${String(event.data?.status ?? '?')}`));
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=(\$(compgen -W "--help --version" -- \${cur}))
16
+ COMPREPLY=($(compgen -W "--help --version" -- \${cur}))
17
17
  else
18
- COMPREPLY=(\$(compgen -W "\${opts}" -- \${cur}))
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