codehere 1.0.1 → 1.0.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 (163) hide show
  1. package/README.md +36 -192
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,99 +0,0 @@
1
- /**
2
- * Vibe-coder-specific danger checks — the high-consequence things gitleaks + semgrep DON'T catch by
3
- * default but that the research named as the #1 ways AI-built apps get owned. Deterministic, zero
4
- * model tokens. Pattern-based on the source files. See docs/PRODUCT_10X.md (step 1 of the plan).
5
- */
6
- import { readdirSync, readFileSync } from 'node:fs';
7
- import { join, extname, relative } from 'node:path';
8
- const SRC_EXT = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte', '.html', '.astro']);
9
- const SKIP_DIR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'vendor', 'coverage', '.turbo']);
10
- function walk(dir, files, depth = 0) {
11
- if (depth > 8)
12
- return;
13
- let ents;
14
- try {
15
- ents = readdirSync(dir, { withFileTypes: true });
16
- }
17
- catch {
18
- return;
19
- }
20
- for (const e of ents) {
21
- if (SKIP_DIR.has(e.name))
22
- continue;
23
- const p = join(dir, e.name);
24
- if (e.isDirectory())
25
- walk(p, files, depth + 1);
26
- else if (SRC_EXT.has(extname(e.name)))
27
- files.push(p);
28
- }
29
- }
30
- const CHECKS = [
31
- {
32
- // The real escalation danger: a PRIVILEGE flag in browser storage (not a token, which is standard).
33
- id: 'privilege-flag-in-browser-storage',
34
- re: /(localStorage|sessionStorage)\s*\.\s*setItem\s*\(\s*['"`](role|is_?admin|admin|is_?staff|is_?paid|paid|premium|pro|plan|tier|permission|access_?level|can_|entitlement)/i,
35
- tier: 'critical',
36
- what: (f) => `A permission flag (admin/role/plan) is stored in the browser in ${f}. A user can open dev-tools, change it, and give themselves admin or paid access — your app trusts a value the user controls. Decide permissions on the server, never from localStorage.`,
37
- },
38
- {
39
- id: 'supabase-service-role-key',
40
- re: /service_role|SUPABASE_SERVICE_ROLE/i,
41
- tier: 'critical',
42
- what: (f) => `A Supabase service_role key is referenced in ${f}. That key bypasses ALL your database rules — if it reaches the browser, anyone can read and write your entire database. It must live only on your server, never in client code.`,
43
- },
44
- {
45
- // User input string-built into a SQL query — concat ("...WHERE id="+x) or interpolation (`...${x}`).
46
- // Parameterized queries ($1 / ? placeholders) don't match, so this stays low-noise.
47
- id: 'sql-injection-string-building',
48
- re: /\b(SELECT\b[^;\n'"`]*\bFROM\b|INSERT\b[^;\n'"`]*\bINTO\b|UPDATE\b[^;\n'"`]*\bSET\b|DELETE\b[^;\n'"`]*\bFROM\b)[^;\n]*?(\$\{|['"`]\s*\+\s*\w)/i,
49
- tier: 'critical',
50
- what: (f) => `User input is being string-built into a SQL query in ${f}. An attacker can put SQL inside that input and read, change, or wipe your entire database. Use parameterized queries (placeholders like $1 or ?), never glue the query together with + or \${}.`,
51
- },
52
- {
53
- id: 'unsafe-html-injection',
54
- re: /dangerouslySetInnerHTML|v-html\s*=|\.innerHTML\s*=\s*[^'"`;\s]/,
55
- tier: 'high',
56
- what: (f) => `Raw HTML is written into the page in ${f}. If any of it comes from a user, an attacker can run scripts in your visitors' browsers — steal their logins, deface the page, or act as them.`,
57
- },
58
- {
59
- id: 'auth-disabled-or-todo',
60
- re: /\/\/\s*(TODO|FIXME).*(auth|login|permission|secur)|auth\s*[:=]\s*false|requireAuth\s*[:=]\s*false|skipAuth\s*[:=]\s*true/i,
61
- tier: 'high',
62
- what: (f) => `Authentication looks turned off or left as a TODO in ${f}. AI agents often stub auth out to "make it work" and never finish it — anyone could reach pages or data meant to be protected.`,
63
- },
64
- ];
65
- /** Scan the source files in `dir` for vibe-coder-specific dangers. One hit per check per file. */
66
- export function vibeChecks(dir) {
67
- const files = [];
68
- walk(dir, files);
69
- const out = [];
70
- for (const f of files) {
71
- let txt;
72
- try {
73
- txt = readFileSync(f, 'utf8');
74
- }
75
- catch {
76
- continue;
77
- }
78
- if (txt.length > 500_000)
79
- continue; // skip huge/minified files
80
- const lines = txt.split('\n');
81
- for (const c of CHECKS) {
82
- for (let i = 0; i < lines.length; i++) {
83
- if (c.re.test(lines[i])) {
84
- out.push({
85
- tool: 'codehere',
86
- ruleId: c.id,
87
- file: relative(dir, f),
88
- line: i + 1,
89
- tier: c.tier,
90
- what: c.what(relative(dir, f)),
91
- });
92
- break;
93
- }
94
- }
95
- }
96
- }
97
- return out;
98
- }
99
- //# sourceMappingURL=vibe-checks.js.map
@@ -1,86 +0,0 @@
1
- /**
2
- * Write a file so a reader never sees it half-written.
3
- *
4
- * Why (measured 2026-08-08, not assumed): `writeFileSync` is open(O_TRUNC) then
5
- * write. SIGKILL landing between the two leaves a truncated, unparseable file.
6
- * Induced deliberately — 40 attempts at a ~290KB payload, **15 came back torn**.
7
- * The window shrinks with file size but never closes, and codehere's state files
8
- * only grow.
9
- *
10
- * The failure this prevents is not "the write was lost" — losing the write is
11
- * survivable, the caller can write again. It is "the file that survived is
12
- * garbage": a corrupt permissions list, an unreadable budget ceiling, an OAuth
13
- * store that no longer parses. `rename(2)` is atomic on POSIX, so a reader sees
14
- * either the whole old file or the whole new one, never a splice of both.
15
- *
16
- * NOT what this gives you: durability across power loss. That needs fsync on the
17
- * file AND its directory, which costs a real syscall on every write. The failure
18
- * mode there is different and much less bad — you lose the write, you do not get
19
- * a corrupt file — so it is deliberately not paid for here. Say so rather than
20
- * implying this makes writes crash-proof.
21
- *
22
- * Appends are NOT routed through this and do not need to be. `appendFileSync`
23
- * opens O_APPEND, and each write is atomic with respect to the file offset:
24
- * measured with 6 concurrent writers × 300 records, and again at 90KB records,
25
- * with zero interleaving or loss both times. The JSONL chain and session logs
26
- * were never at risk from this, and an earlier audit that counted appends
27
- * alongside whole-file writes overstated the problem.
28
- */
29
- import { writeFileSync, renameSync, unlinkSync, readdirSync, statSync } from 'node:fs';
30
- import { dirname, basename, join } from 'node:path';
31
- /** A temp older than this was orphaned by a crash, not left by a live write. */
32
- const STALE_TEMP_MS = 60_000;
33
- /**
34
- * Sweep temps this file's own writes orphaned. SIGKILL cannot run a catch block,
35
- * so a hard crash mid-write always leaves one behind — measured at 16 after 40
36
- * kills. Without this, trading a corrupt file for permanent litter in the user's
37
- * state dir just moves the problem: a stray `.tmp` next to `permissions.yaml`
38
- * reads as corruption to anyone opening the directory.
39
- *
40
- * Scoped to the target's own basename so it can never touch another file's
41
- * in-flight temp, and age-gated so it can never race a concurrent writer.
42
- */
43
- function sweepStaleTemps(path) {
44
- const dir = dirname(path);
45
- const prefix = `${basename(path)}.`;
46
- try {
47
- for (const name of readdirSync(dir)) {
48
- if (!name.startsWith(prefix) || !name.endsWith('.tmp'))
49
- continue;
50
- const p = join(dir, name);
51
- try {
52
- if (Date.now() - statSync(p).mtimeMs > STALE_TEMP_MS)
53
- unlinkSync(p);
54
- }
55
- catch {
56
- /* vanished under us, or not ours to remove — either way, leave it */
57
- }
58
- }
59
- }
60
- catch {
61
- /* unreadable dir · sweeping is hygiene, never a reason to fail the write */
62
- }
63
- }
64
- export function writeFileAtomic(path, data, opts) {
65
- // pid + time keeps two concurrent writers of the SAME path from clobbering
66
- // each other's temp file; the rename still decides who wins, and either whole
67
- // file is valid.
68
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
69
- try {
70
- writeFileSync(tmp, data, opts?.mode !== undefined ? { mode: opts.mode } : undefined);
71
- renameSync(tmp, path);
72
- sweepStaleTemps(path);
73
- }
74
- catch (err) {
75
- // Never leave the temp behind on a failed write — it would accumulate in the
76
- // user's state dir forever and look like corruption to anyone reading it.
77
- try {
78
- unlinkSync(tmp);
79
- }
80
- catch {
81
- /* the temp never existed, or is already gone */
82
- }
83
- throw err;
84
- }
85
- }
86
- //# sourceMappingURL=atomic-write.js.map
@@ -1,268 +0,0 @@
1
- /**
2
- * Cross-tool agent definition reader — v0.6.0 M8.
3
- *
4
- * Reads agent definitions written for OTHER tools (OpenCode, Claude Code) and
5
- * normalizes them into codehere's internal shape so they appear in the agents
6
- * pane alongside codehere's built-in adapters.
7
- *
8
- * Built from Spike-4 prototype (agent/spike/v0.6.0-cross-tool-parser.mjs);
9
- * production version uses the `yaml` lib for proper 2-level nesting + multi-
10
- * line scalars + quoted-string handling, recurses subdirectories per the
11
- * AGENTS.md hierarchical convention, skips files without frontmatter, and
12
- * surfaces unknown frontmatter keys as warnings (not silent drops).
13
- *
14
- * Sources read in precedence order (later overrides earlier on name conflict):
15
- * 1. ~/.claude/agents/ (global Claude Code subagents)
16
- * 2. ~/.opencode/agents/ (global OpenCode agents)
17
- * 3. ~/.codehere/agents/ (global codehere agents)
18
- * 4. <cwd>/.claude/agents/ (project Claude Code)
19
- * 5. <cwd>/.opencode/agents/ (project OpenCode)
20
- * 6. <cwd>/.codehere/agents/ (project codehere)
21
- *
22
- * Format normalization handles divergences:
23
- * - Claude `name` field vs OpenCode (filename = name)
24
- * - Claude `tools: "A, B, C"` (string) vs OpenCode `tools: {a: true, b: false}` (object)
25
- * - Claude no `mode` (subagent implicit) vs OpenCode `mode: primary|subagent|all`
26
- * - OpenCode `permission` block vs Claude (no permission block)
27
- *
28
- * NOT in scope of this module:
29
- * - Permission ENFORCEMENT (this just READS them for display + future M5)
30
- * - Template variable substitution ({{PROJECT_NAME}} stays as raw text)
31
- * - Authoring (`codehere agent create` is C6, deferred)
32
- */
33
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
34
- import { join } from 'path';
35
- import { homedir } from 'os';
36
- import YAML from 'yaml';
37
- /**
38
- * Pre-process Claude Code-style frontmatter that contains unquoted colons in
39
- * description/prompt values (e.g. "description: ... Examples:\n<example>\nContext: ...").
40
- * Strict YAML parsers reject these; Claude Code itself uses a more permissive
41
- * reader. We force-quote affected values before parsing.
42
- *
43
- * Heuristic: any top-level `description:` or `prompt:` line whose value
44
- * contains a `: ` (colon-space) AND isn't already quoted/block-scalar gets
45
- * single-quoted with internal single quotes doubled (YAML escape).
46
- */
47
- function preprocessFrontmatter(fmText) {
48
- return fmText
49
- .split('\n')
50
- .map((line) => {
51
- const m = line.match(/^(description|prompt):\s*(.+)$/);
52
- if (!m)
53
- return line;
54
- const [, key, value] = m;
55
- const trimmed = value.trim();
56
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
57
- (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
58
- trimmed.startsWith('|') ||
59
- trimmed.startsWith('>')) {
60
- return line; // already a safe scalar form
61
- }
62
- if (!/:\s/.test(trimmed))
63
- return line; // no problematic embedded colons
64
- const escaped = trimmed.replace(/'/g, "''");
65
- return `${key}: '${escaped}'`;
66
- })
67
- .join('\n');
68
- }
69
- /** Recursively walk a dir, return all .md files (sorted for determinism). */
70
- function listMdFilesRecursive(dir) {
71
- if (!existsSync(dir))
72
- return [];
73
- let stat;
74
- try {
75
- stat = statSync(dir);
76
- }
77
- catch {
78
- return [];
79
- }
80
- if (!stat.isDirectory())
81
- return [];
82
- const out = [];
83
- let entries;
84
- try {
85
- entries = readdirSync(dir);
86
- }
87
- catch {
88
- return [];
89
- }
90
- for (const entry of entries.sort()) {
91
- if (entry.startsWith('.'))
92
- continue;
93
- const p = join(dir, entry);
94
- let s;
95
- try {
96
- s = statSync(p);
97
- }
98
- catch {
99
- continue;
100
- }
101
- if (s.isDirectory()) {
102
- out.push(...listMdFilesRecursive(p));
103
- }
104
- else if (s.isFile() && entry.endsWith('.md') && entry !== 'README.md') {
105
- out.push(p);
106
- }
107
- }
108
- return out;
109
- }
110
- /** Parse a single agent file. Returns null if no frontmatter or parse fails. */
111
- export function parseAgentFile(filePath, defaultFormat = 'unknown') {
112
- let text;
113
- try {
114
- text = readFileSync(filePath, 'utf-8');
115
- }
116
- catch {
117
- return null;
118
- }
119
- if (!text.startsWith('---'))
120
- return null; // no frontmatter, not an agent file
121
- const closeIdx = text.indexOf('\n---', 4);
122
- if (closeIdx === -1)
123
- return null;
124
- const fmText = text.slice(4, closeIdx).trim();
125
- const body = text.slice(closeIdx + 4).trim();
126
- let fm;
127
- try {
128
- fm = YAML.parse(preprocessFrontmatter(fmText));
129
- if (!fm || typeof fm !== 'object')
130
- return null;
131
- }
132
- catch {
133
- return null;
134
- }
135
- // Source format inference: presence of mode/permission/object-tools = OpenCode;
136
- // presence of name + string-tools = Claude. Fall back to default by location.
137
- const hasObjectTools = typeof fm.tools === 'object' && fm.tools !== null && !Array.isArray(fm.tools);
138
- const sourceFormat = fm.mode || fm.permission || hasObjectTools
139
- ? 'opencode'
140
- : fm.name || typeof fm.tools === 'string'
141
- ? 'claude'
142
- : defaultFormat;
143
- // Tools normalization
144
- let tools = null;
145
- if (typeof fm.tools === 'string') {
146
- tools = fm.tools
147
- .split(',')
148
- .map((t) => t.trim().toLowerCase())
149
- .filter(Boolean);
150
- }
151
- else if (hasObjectTools) {
152
- tools = [];
153
- for (const [k, v] of Object.entries(fm.tools)) {
154
- if (v === true)
155
- tools.push(k.toLowerCase());
156
- }
157
- }
158
- // Filename without .md = fallback name
159
- const filenameName = filePath.split('/').pop()?.replace(/\.md$/, '') ?? 'unnamed';
160
- // Mode normalization
161
- const modeRaw = typeof fm.mode === 'string' ? fm.mode : null;
162
- const mode = modeRaw === 'primary' || modeRaw === 'subagent' || modeRaw === 'all'
163
- ? modeRaw
164
- : sourceFormat === 'claude'
165
- ? 'subagent'
166
- : 'all';
167
- // Track unknown frontmatter keys (everything outside our normalized 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
- ]);
183
- const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
184
- return {
185
- name: typeof fm.name === 'string' ? fm.name : filenameName,
186
- description: typeof fm.description === 'string' ? fm.description : '(no description)',
187
- sourceFormat,
188
- sourcePath: filePath,
189
- mode,
190
- model: typeof fm.model === 'string' ? fm.model : null,
191
- temperature: typeof fm.temperature === 'number' ? fm.temperature : null,
192
- tools,
193
- permission: typeof fm.permission === 'object' && fm.permission !== null ? fm.permission : null,
194
- color: typeof fm.color === 'string' ? fm.color : null,
195
- hidden: fm.hidden === true,
196
- body,
197
- unknownKeys,
198
- };
199
- }
200
- /** All locations to scan, in precedence order (later overrides earlier on name conflict).
201
- * Dedupes the dir list — when cwd == homeDir, project + global paths collapse and we
202
- * shouldn't double-count files. Test isolation also relies on this (homeDir override
203
- * to a project root). */
204
- function agentDirsToScan(cwd, homeOverride) {
205
- const home = homeOverride ?? homedir();
206
- const dirs = [
207
- { dir: join(home, '.claude/agents'), defaultFormat: 'claude' },
208
- { dir: join(home, '.opencode/agents'), defaultFormat: 'opencode' },
209
- { dir: join(home, '.codehere/agents'), defaultFormat: 'codehere' },
210
- ];
211
- if (cwd) {
212
- dirs.push({ dir: join(cwd, '.claude/agents'), defaultFormat: 'claude' });
213
- dirs.push({ dir: join(cwd, '.opencode/agents'), defaultFormat: 'opencode' });
214
- dirs.push({ dir: join(cwd, '.codehere/agents'), defaultFormat: 'codehere' });
215
- }
216
- // Dedupe by dir path (preserves order — first occurrence wins)
217
- const seen = new Set();
218
- return dirs.filter((d) => {
219
- if (seen.has(d.dir))
220
- return false;
221
- seen.add(d.dir);
222
- return true;
223
- });
224
- }
225
- /** Read all cross-tool agent definitions from global + project dirs. Idempotent + cheap (~10ms for ~20 files); safe to call on every readiness query.
226
- *
227
- * @param cwd Project working directory whose .claude/.opencode/.codehere agents subdirs to scan
228
- * @param options.homeDir Override home directory (testing only — production uses homedir())
229
- */
230
- export function readCrossToolAgents(cwd, options) {
231
- const byName = new Map();
232
- let filesParsed = 0;
233
- let filesSkippedNoFrontmatter = 0;
234
- let filesSkippedParseError = 0;
235
- let nameCollisions = 0;
236
- const dirs = agentDirsToScan(cwd, options?.homeDir);
237
- for (const { dir, defaultFormat } of dirs) {
238
- for (const filePath of listMdFilesRecursive(dir)) {
239
- let agent;
240
- try {
241
- agent = parseAgentFile(filePath, defaultFormat);
242
- }
243
- catch {
244
- filesSkippedParseError++;
245
- continue;
246
- }
247
- if (!agent) {
248
- filesSkippedNoFrontmatter++;
249
- continue;
250
- }
251
- filesParsed++;
252
- if (byName.has(agent.name))
253
- nameCollisions++;
254
- byName.set(agent.name, agent); // later wins
255
- }
256
- }
257
- return {
258
- agents: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)),
259
- diagnostics: {
260
- sourcesScanned: dirs.length,
261
- filesParsed,
262
- filesSkippedNoFrontmatter,
263
- filesSkippedParseError,
264
- nameCollisions,
265
- },
266
- };
267
- }
268
- //# sourceMappingURL=cross-tool-agents.js.map
@@ -1,237 +0,0 @@
1
- /**
2
- * Project thread migration — sessions.jsonl → projects/<slug>/thread.jsonl.
3
- *
4
- * v0.6.0 M1+M7 foundation. The new schema groups sessions by their cwd
5
- * (project) so the persistent project thread shape (one continuous chat per
6
- * project) has a place to live. The original sessions.jsonl is preserved
7
- * for one release as back-compat.
8
- *
9
- * Built from Spike-1 prototype (agent/spike/v0.6.0-migrate-sessions-to-threads.mjs).
10
- * Spike validated lossless + reversible on 612 real sessions in 5ms.
11
- *
12
- * Production additions over the spike:
13
- * - Atomic per-project writes (.tmp + rename) so partial failures don't
14
- * leave half-written thread.jsonl files
15
- * - Idempotent: skips writing if target thread.jsonl already exists
16
- * (re-run on daemon start is safe)
17
- * - Temp-dir filter: skips /tmp/, /var/folders/, /private/var/folders/ by
18
- * default (configurable) — these are test artifacts, not real projects
19
- * - Abandoned-session marker: sessions with no completed event get
20
- * status='abandoned' instead of silent null
21
- * - Configurable input/output paths so tests can use temp fixtures
22
- *
23
- * NOT in scope for this module (handled by callers):
24
- * - CLI interface (codehere migrate-threads — TBD subcommand)
25
- * - Auto-run on daemon start
26
- * - Forward-mirroring (writing to thread.jsonl alongside sessions.jsonl
27
- * during normal daemon operation)
28
- *
29
- * See docs/research/v0.6.0-spike-1-migration.md for the validation history.
30
- */
31
- import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, renameSync } from 'fs';
32
- import { join } from 'path';
33
- import { createHash } from 'crypto';
34
- /** Path patterns considered ephemeral and skipped from project list. */
35
- const DEFAULT_TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
36
- /** sha1(cwd)[:12] — stable, short, filesystem-safe. */
37
- function slugForCwd(cwd, unknownLabel) {
38
- if (!cwd)
39
- return unknownLabel;
40
- return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
41
- }
42
- function isTempDir(cwd) {
43
- if (!cwd)
44
- return false;
45
- return DEFAULT_TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
46
- }
47
- /** Read JSONL events; tolerate malformed lines (count + skip). */
48
- function readEvents(path) {
49
- if (!existsSync(path))
50
- return { events: [], malformed: 0 };
51
- const text = readFileSync(path, 'utf-8');
52
- const lines = text.split('\n').filter((l) => l.trim());
53
- const events = [];
54
- let malformed = 0;
55
- for (const line of lines) {
56
- try {
57
- events.push(JSON.parse(line));
58
- }
59
- catch {
60
- malformed++;
61
- }
62
- }
63
- return { events, malformed };
64
- }
65
- /** Roll up event-log entries into one record per session. */
66
- function rollupSessions(events) {
67
- const byId = new Map();
68
- const completedIds = new Set();
69
- for (const e of events) {
70
- const id = (e.id ?? e.sessionId);
71
- if (!id)
72
- continue;
73
- if (e.t === 'completed' || e.completedAt !== undefined)
74
- completedIds.add(id);
75
- const existing = byId.get(id) ?? { id, abandoned: true };
76
- const existingAny = existing;
77
- for (const k of [
78
- 'agent',
79
- 'prompt',
80
- 'cwd',
81
- 'startedAt',
82
- 'status',
83
- 'durationMs',
84
- 'completedAt',
85
- 'steps',
86
- 'artifacts',
87
- ]) {
88
- if (e[k] !== undefined && existingAny[k] === undefined) {
89
- existingAny[k] = e[k];
90
- }
91
- }
92
- byId.set(id, existing);
93
- }
94
- // Mark abandoned (sessions with no completed event)
95
- for (const s of byId.values()) {
96
- s.abandoned = !completedIds.has(s.id);
97
- if (s.abandoned && !s.status)
98
- s.status = 'abandoned';
99
- }
100
- return [...byId.values()];
101
- }
102
- function groupByCwd(sessions, unknownLabel) {
103
- const groups = new Map();
104
- for (const s of sessions) {
105
- const cwd = s.cwd ?? null;
106
- const slug = slugForCwd(cwd, unknownLabel);
107
- const group = groups.get(slug) ?? { slug, cwd, sessions: [] };
108
- group.sessions.push(s);
109
- groups.set(slug, group);
110
- }
111
- return [...groups.values()];
112
- }
113
- /** Atomic write: write to .tmp then rename. Caller ensures dir exists. */
114
- function atomicWriteFile(path, content) {
115
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
116
- writeFileSync(tmp, content);
117
- renameSync(tmp, path);
118
- }
119
- function buildThreadJsonl(group) {
120
- const lines = [];
121
- lines.push(JSON.stringify({
122
- _meta: 'project',
123
- slug: group.slug,
124
- cwd: group.cwd,
125
- sessionCount: group.sessions.length,
126
- migratedFrom: 'sessions.jsonl',
127
- migratedAt: Date.now(),
128
- }));
129
- const sorted = [...group.sessions].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
130
- for (const s of sorted) {
131
- lines.push(JSON.stringify({
132
- type: 'task',
133
- sessionId: s.id,
134
- agent: s.agent,
135
- prompt: s.prompt,
136
- cwd: s.cwd,
137
- startedAt: s.startedAt,
138
- status: s.status,
139
- durationMs: s.durationMs,
140
- completedAt: s.completedAt,
141
- steps: s.steps,
142
- artifacts: s.artifacts,
143
- }));
144
- }
145
- return lines.join('\n') + '\n';
146
- }
147
- /**
148
- * Run the migration. Idempotent: skips projects whose thread.jsonl already
149
- * exists (so daemon-startup auto-runs are safe).
150
- */
151
- export function migrate(options) {
152
- const startedAt = Date.now();
153
- const skipTempDirs = options.skipTempDirs ?? true;
154
- const unknownLabel = options.unknownCwdLabel ?? 'unknown';
155
- const { events } = readEvents(options.sourceJsonl);
156
- const sessions = rollupSessions(events);
157
- const groups = groupByCwd(sessions, unknownLabel);
158
- let threadsWritten = 0;
159
- let threadsSkippedExisting = 0;
160
- let unknownCwdSessions = 0;
161
- const abandonedSessions = sessions.filter((s) => s.abandoned).length;
162
- let tempDirSessionsSkipped = 0;
163
- for (const group of groups) {
164
- if (skipTempDirs && isTempDir(group.cwd)) {
165
- tempDirSessionsSkipped += group.sessions.length;
166
- continue;
167
- }
168
- if (group.cwd === null)
169
- unknownCwdSessions += group.sessions.length;
170
- const dir = join(options.targetDir, group.slug);
171
- const threadFile = join(dir, 'thread.jsonl');
172
- if (existsSync(threadFile)) {
173
- threadsSkippedExisting++;
174
- continue;
175
- }
176
- mkdirSync(dir, { recursive: true });
177
- atomicWriteFile(threadFile, buildThreadJsonl(group));
178
- threadsWritten++;
179
- }
180
- return {
181
- sessionsProcessed: sessions.length,
182
- threadsWritten,
183
- threadsSkippedExisting,
184
- unknownCwdSessions,
185
- abandonedSessions,
186
- tempDirSessionsSkipped,
187
- durationMs: Date.now() - startedAt,
188
- };
189
- }
190
- /** Dry-run: same as migrate but writes nothing. Returns what WOULD happen. */
191
- export function dryRun(options) {
192
- const startedAt = Date.now();
193
- const skipTempDirs = options.skipTempDirs ?? true;
194
- const unknownLabel = options.unknownCwdLabel ?? 'unknown';
195
- const { events } = readEvents(options.sourceJsonl);
196
- const sessions = rollupSessions(events);
197
- const groups = groupByCwd(sessions, unknownLabel);
198
- let threadsWritten = 0;
199
- let threadsSkippedExisting = 0;
200
- let unknownCwdSessions = 0;
201
- const abandonedSessions = sessions.filter((s) => s.abandoned).length;
202
- let tempDirSessionsSkipped = 0;
203
- for (const group of groups) {
204
- if (skipTempDirs && isTempDir(group.cwd)) {
205
- tempDirSessionsSkipped += group.sessions.length;
206
- continue;
207
- }
208
- if (group.cwd === null)
209
- unknownCwdSessions += group.sessions.length;
210
- const threadFile = join(options.targetDir, group.slug, 'thread.jsonl');
211
- if (existsSync(threadFile))
212
- threadsSkippedExisting++;
213
- else
214
- threadsWritten++;
215
- }
216
- return {
217
- sessionsProcessed: sessions.length,
218
- threadsWritten,
219
- threadsSkippedExisting,
220
- unknownCwdSessions,
221
- abandonedSessions,
222
- tempDirSessionsSkipped,
223
- durationMs: Date.now() - startedAt,
224
- };
225
- }
226
- /**
227
- * Rollback: removes the projects directory. Source sessions.jsonl is never
228
- * touched. Returns the count of removed project subdirectories.
229
- */
230
- export function rollback(targetDir) {
231
- if (!existsSync(targetDir))
232
- return { removedDirs: 0 };
233
- const subdirs = readdirSync(targetDir);
234
- rmSync(targetDir, { recursive: true });
235
- return { removedDirs: subdirs.length };
236
- }
237
- //# sourceMappingURL=project-thread-migration.js.map