@zuzuucodes/cli 1.0.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/bin/zuzuu.mjs +133 -0
  4. package/experiments/experiment-1-trace-capture/adapters/claude-code.mjs +220 -0
  5. package/experiments/experiment-1-trace-capture/adapters/codex.mjs +201 -0
  6. package/experiments/experiment-1-trace-capture/adapters/gemini-cli.mjs +113 -0
  7. package/experiments/experiment-1-trace-capture/adapters/host-adapter.mjs +43 -0
  8. package/experiments/experiment-1-trace-capture/adapters/opencode.mjs +205 -0
  9. package/experiments/experiment-1-trace-capture/adapters/pi.mjs +218 -0
  10. package/experiments/experiment-1-trace-capture/adapters/registry.mjs +20 -0
  11. package/experiments/experiment-1-trace-capture/adapters/signals.mjs +44 -0
  12. package/experiments/experiment-1-trace-capture/core/event.mjs +58 -0
  13. package/experiments/experiment-1-trace-capture/core/ids.mjs +32 -0
  14. package/experiments/experiment-1-trace-capture/core/otlp.mjs +54 -0
  15. package/experiments/experiment-1-trace-capture/core/render.mjs +63 -0
  16. package/experiments/experiment-1-trace-capture/core/spans.mjs +43 -0
  17. package/package.json +56 -0
  18. package/zuzuu/actions/adapter.mjs +130 -0
  19. package/zuzuu/actions/convert.mjs +27 -0
  20. package/zuzuu/actions/dispatch.mjs +87 -0
  21. package/zuzuu/actions/inbox.mjs +56 -0
  22. package/zuzuu/actions/manifest.mjs +72 -0
  23. package/zuzuu/actions/marker.mjs +4 -0
  24. package/zuzuu/actions/runner.mjs +37 -0
  25. package/zuzuu/actions/schema.mjs +73 -0
  26. package/zuzuu/actions/trail.mjs +22 -0
  27. package/zuzuu/capture-core.mjs +49 -0
  28. package/zuzuu/commands/act-author.mjs +72 -0
  29. package/zuzuu/commands/act.mjs +101 -0
  30. package/zuzuu/commands/capture.mjs +32 -0
  31. package/zuzuu/commands/code.mjs +84 -0
  32. package/zuzuu/commands/digest.mjs +23 -0
  33. package/zuzuu/commands/distill.mjs +46 -0
  34. package/zuzuu/commands/doctor.mjs +197 -0
  35. package/zuzuu/commands/enable.mjs +195 -0
  36. package/zuzuu/commands/eval.mjs +101 -0
  37. package/zuzuu/commands/explain.mjs +119 -0
  38. package/zuzuu/commands/generation.mjs +107 -0
  39. package/zuzuu/commands/hook.mjs +209 -0
  40. package/zuzuu/commands/inbox.mjs +73 -0
  41. package/zuzuu/commands/init.mjs +89 -0
  42. package/zuzuu/commands/knowledge.mjs +152 -0
  43. package/zuzuu/commands/migrate.mjs +125 -0
  44. package/zuzuu/commands/review.mjs +299 -0
  45. package/zuzuu/commands/status.mjs +82 -0
  46. package/zuzuu/commands/trace.mjs +19 -0
  47. package/zuzuu/digest.mjs +149 -0
  48. package/zuzuu/eval/rank.mjs +31 -0
  49. package/zuzuu/eval/score.mjs +85 -0
  50. package/zuzuu/eval/signals.mjs +57 -0
  51. package/zuzuu/faculty/contract.mjs +19 -0
  52. package/zuzuu/faculty/gate.mjs +65 -0
  53. package/zuzuu/faculty/generation.mjs +392 -0
  54. package/zuzuu/faculty/proposal.mjs +166 -0
  55. package/zuzuu/faculty/provenance.mjs +35 -0
  56. package/zuzuu/faculty/registry.mjs +33 -0
  57. package/zuzuu/faculty/trail.mjs +27 -0
  58. package/zuzuu/guardrails/adapter.mjs +134 -0
  59. package/zuzuu/guardrails.mjs +89 -0
  60. package/zuzuu/inject.mjs +46 -0
  61. package/zuzuu/instructions/adapter.mjs +93 -0
  62. package/zuzuu/knowledge/adapter.mjs +99 -0
  63. package/zuzuu/knowledge/distill.mjs +237 -0
  64. package/zuzuu/knowledge/embed.mjs +52 -0
  65. package/zuzuu/knowledge/er.mjs +98 -0
  66. package/zuzuu/knowledge/inbox.mjs +43 -0
  67. package/zuzuu/knowledge/index.mjs +194 -0
  68. package/zuzuu/knowledge/items.mjs +154 -0
  69. package/zuzuu/knowledge/proposals.mjs +196 -0
  70. package/zuzuu/knowledge/registry.mjs +115 -0
  71. package/zuzuu/live/install.mjs +76 -0
  72. package/zuzuu/live/live-store.mjs +78 -0
  73. package/zuzuu/live/probe.mjs +55 -0
  74. package/zuzuu/live/reconcile.mjs +33 -0
  75. package/zuzuu/memory/adapter.mjs +121 -0
  76. package/zuzuu/miners/actions.mjs +118 -0
  77. package/zuzuu/miners/guardrails.mjs +174 -0
  78. package/zuzuu/miners/instructions.mjs +152 -0
  79. package/zuzuu/miners/knowledge.mjs +22 -0
  80. package/zuzuu/miners/memory.mjs +27 -0
  81. package/zuzuu/miners/registry.mjs +31 -0
  82. package/zuzuu/scaffold.mjs +213 -0
  83. package/zuzuu/session.mjs +72 -0
  84. package/zuzuu/store.mjs +104 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harshit Krishna Choudhary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # zuzuu
2
+
3
+ [![ci](https://github.com/h1902y/zuzuu/actions/workflows/ci.yml/badge.svg)](https://github.com/h1902y/zuzuu/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/@zuzuucodes/cli)](https://www.npmjs.com/package/@zuzuucodes/cli) [![node](https://img.shields.io/node/v/@zuzuucodes/cli)](package.json) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
4
+
5
+ **Give the coding agent you already run an evolving Memory, Knowledge, Actions, and Guardrails — grown from how you actually work.**
6
+
7
+ Your host agent — Claude Code, Codex, Gemini CLI, OpenCode — supplies the *brain* (the reasoning loop + the model). zuzuu wraps the host you already pay for: it **serves** faculties to it, **observes** every session as an OpenTelemetry trace, and (the end-game) **evolves** the faculties from those traces — human-gated, across versioned generations. We never run a competing agent loop and never drive the host headlessly.
8
+
9
+ > The CLI is `zuzuu` (package `zuzuu`, v1.0.0).
10
+
11
+ > **Status (honest):** early build, moving fast. **Observe** works (5 real hosts, verified). **Serve** delivers the faculty home (`zuzuu init`), a session digest to every host, an **enforced guardrails gate** on all 5, and five faculties sharing one proposal/review spine. **Evolve** is now **wired and tested** — trace miners → a mechanical eval lens → human-gated `zuzuu review` → versioned **generations** (mint / rollback / drift-check) — but **not yet proven on a real graduation corpus** (the loop runs + passes hermetic tests; it hasn't yet improved an agent from real sessions end-to-end). Full design: [`docs/DESIGN.md`](docs/DESIGN.md).
12
+
13
+ ## What works today
14
+
15
+ ```bash
16
+ npm install -g @zuzuucodes/cli # zero dependencies — installs the `zuzuu` command
17
+
18
+ # no coding agent yet? one command gives you a fully faculty-equipped one:
19
+ zuzuu code # scaffold the faculty home, install + wire OpenCode, launch it (capture + gate + grounding)
20
+
21
+ # already run Claude Code / Gemini / Codex / OpenCode / pi? wrap the one you have:
22
+ zuzuu init # scaffold your project's agent home (agent/) — git-style, open
23
+ zuzuu explain # the 5 faculties + how graduation works (you're always in the loop)
24
+ zuzuu inbox # what's pending your approval · zuzuu review to approve/reject
25
+ zuzuu capture # turn your latest agent session into an OpenTelemetry trace
26
+ zuzuu trace --last
27
+ zuzuu enable [--host gemini-cli|codex|opencode|pi] # live capture + the guardrails gate
28
+ zuzuu doctor # health + lost-session reconciliation
29
+ ```
30
+
31
+ `zuzuu code` is the **bundled-host** path (Stage 2): it detects OpenCode (installs it on first run, with your OK — never an npm dependency, the zero-dep policy holds), wires the zuzuu plugin, and launches the real `opencode` — we configure + launch, never fork or drive it.
32
+
33
+ | | Claude Code | Gemini CLI | Codex | OpenCode | pi |
34
+ |---|---|---|---|---|---|
35
+ | post-hoc capture | ✅ rich | ✅ thin | ✅ rich | ✅ rich | ✅ rich |
36
+ | live capture | ✅ hooks | ✅ hooks | ✅ hooks¹ | ✅ plugin | ✅ extension |
37
+ | guardrails gate | ✅ PreToolUse | ✅ BeforeTool | ✅ PreToolUse¹ | ✅ tool.execute.before | ✅ tool_call |
38
+
39
+ ¹ **Codex is interactive-only** — `codex exec` (headless) fires no hooks (verified, v0.138.0), so live capture + gate work when you run Codex interactively; headless Codex still gets post-hoc `zuzuu capture`.
40
+
41
+ All five verified against **real sessions** — never fixtures; every host's live capture + gate was wired from **real captured hook payloads** and dogfooded end-to-end ([`experiments/LOG.md`](experiments/LOG.md) exp-11 Gemini/Codex, exp-12 OpenCode/pi). Gate semantics are host-honest: deny hard-blocks everywhere; `ask` maps to a native prompt on Claude, defers to the host elsewhere.
42
+
43
+ **Prerequisites:** Node ≥ 22 — that's it. You need at least one supported agent you've already used, so a session exists to capture. (Hacking on zuzuu itself? `git clone https://github.com/h1902y/zuzuu && cd zuzuu && npm link`.)
44
+
45
+ **`zuzuu init`** behaves like `git init`: empty dir → scaffolds the agent home + `AGENTS.md`/`CLAUDE.md`; existing project → adds `agent/` and injects a small delimiter-marked block into your existing instruction files (your text is never touched); already initialized → restores missing pieces only. The home is **open and self-explaining** — a visible `agent/` dir you can read and version in git: `agent/README.md` (the explainer) · `knowledge/` (verified facts) · `memory/` (curated episodes) · `actions/` (runbooks) · `instructions/` (steering) · `guardrails/` (enforced rules), plus `generations/` (your checkpoints). Machine internals are dot-prefixed + git-ignored (`agent/.traces/`, `agent/.live/`).
46
+
47
+ **Live capture** (`zuzuu enable`) is invisible by design: a minimal lifecycle hook set (Claude Code, Gemini CLI, Codex), a bus plugin (OpenCode), or an extension (pi) — each wrapped so it **always exits 0 / fails open — it can never break your agent**. The same hook carries the guardrails gate, applied in each host's own idiom (Claude/Codex `hookSpecificOutput`, Gemini `{decision:"deny"}`, OpenCode throws from `tool.execute.before`, pi returns `{block:true}` from `tool_call`). Most hosts emit no clean end-signal when a terminal is killed, so `zuzuu doctor` *reconciles* lost sessions afterward from the transcript still on disk (nothing lost).
48
+
49
+ **Where your data lives:** transcripts are read **read-only**; output is git-native in your repo — `agent/sessions.json` (small tracked index, each session linked to a commit) + `agent/.traces/*.otlp.jsonl` (local, git-ignored). **Nothing is uploaded**; no raw tool input/output on the trace (byte sizes only).
50
+
51
+ **Verify / troubleshoot:** `npm test` (hermetic) · `npm run playground` (⏭️ skip = that host isn't on *your* machine, not a failure) · `zuzuu doctor` (env + session health). "No host detected" → use a supported agent once in the repo, then retry.
52
+
53
+ ## The idea in one diagram
54
+
55
+ ```
56
+ the host agent (yours) zuzuu
57
+ ┌─────────────────────┐ ┌──────────────────────────────┐
58
+ │ Cognition · Model · │ ◄── │ SERVE faculties: │
59
+ │ Workspace │ │ knowledge · memory · │
60
+ │ (we never drive) │ │ actions · instructions · │
61
+ │ │ │ guardrails (enforced) │
62
+ └──────────┬──────────┘ ├──────────────────────────────┤
63
+ │ sessions │ OBSERVE traces (OTel, │
64
+ └──────────────► │ git-native) │
65
+ ├──────────────────────────────┤
66
+ │ EVOLVE eval → propose → │
67
+ │ human gate → new │
68
+ │ generation [design] │
69
+ └──────────────────────────────┘
70
+ ```
71
+
72
+ **Five faculties**, each mapping onto a cognitive system — **Knowledge** (semantic: what's true), **Memory** (episodic: what happened), **Actions** (procedural: how to do things), **Instructions** (directive: who the agent is), **Guardrails** (protective: what it must not do — *enforced* on tool calls, fail-open). They improve across **versioned generations**, proposals mined from traces, **always human-approved**. That loop is the product; everything here is a step toward it.
73
+
74
+ ## Repo map
75
+
76
+ | Path | What |
77
+ |---|---|
78
+ | [`zuzuu/`](zuzuu/) + `bin/zuzuu.mjs` | the CLI — capture, live lifecycle, faculty home (product surface) |
79
+ | [`experiments/`](experiments/) | spike code + [`LOG.md`](experiments/LOG.md) — the build journal (hypothesis → real-data proof → conclusions per experiment) |
80
+ | [`app/`](app/) | the durable application skeleton (be / run / evolve) — proven code harvests here |
81
+ | [`tests/`](tests/) | hermetic unit + regression (`npm test`) + real-data smoke playgrounds (`npm run playground`) |
82
+ | [`docs/`](docs/) | [`DESIGN.md`](docs/DESIGN.md) (the canon) + [`inspiration/`](docs/inspiration/) (the research shelf: 100-project survey + 5 audits) |
83
+
84
+ ## How this is built (the method)
85
+
86
+ **Experiment → prove on real data → conclude → harvest.** Every capability starts as a numbered experiment with a hypothesis; it must be verified against *real* sessions/wire data (never invented fixtures) before it counts; lessons land in the experiment’s Conclusions section; proven parts graduate into `app/`. Built in public — day-by-day on X ([@h1902y](https://x.com/h1902y)).
87
+
88
+ ## License & status
89
+
90
+ Personal project, early and changing daily. Issues/ideas welcome.
package/bin/zuzuu.mjs ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // zuzuu — the agent-faculty CLI (formerly zuzuu / motors & sensors). Verb-first, entire.io-style; zero deps, no build.
3
+ //
4
+ // zuzuu status detected hosts + recorded sessions
5
+ // zuzuu capture [--host h] capture a session → git-native trace + index entry
6
+ // zuzuu trace [--last | FILE] print a captured trace's span tree
7
+ // zuzuu doctor environment + session health
8
+ // zuzuu version | help
9
+ //
10
+ // Phase 1: post-hoc transcript capture. Phase 2 (planned): `zuzuu enable` installs
11
+ // background hooks for invisible live capture across the agent session lifecycle.
12
+
13
+ import { readFileSync } from 'node:fs';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, join } from 'node:path';
16
+ import { init } from '../zuzuu/commands/init.mjs';
17
+ import { status } from '../zuzuu/commands/status.mjs';
18
+ import { capture } from '../zuzuu/commands/capture.mjs';
19
+ import { trace } from '../zuzuu/commands/trace.mjs';
20
+ import { doctor } from '../zuzuu/commands/doctor.mjs';
21
+ import { enable, disable } from '../zuzuu/commands/enable.mjs';
22
+ import { runHook } from '../zuzuu/commands/hook.mjs';
23
+ import { remember, recall, knowledge } from '../zuzuu/commands/knowledge.mjs';
24
+ import { review, proposals } from '../zuzuu/commands/review.mjs';
25
+ import { distill } from '../zuzuu/commands/distill.mjs';
26
+ import { digest } from '../zuzuu/commands/digest.mjs';
27
+ import { act } from '../zuzuu/commands/act.mjs';
28
+ import { migrate } from '../zuzuu/commands/migrate.mjs';
29
+ import { generation } from '../zuzuu/commands/generation.mjs';
30
+ import { evalCmd } from '../zuzuu/commands/eval.mjs';
31
+ import { code } from '../zuzuu/commands/code.mjs';
32
+ import { explain } from '../zuzuu/commands/explain.mjs';
33
+ import { inbox } from '../zuzuu/commands/inbox.mjs';
34
+
35
+ function parseArgs(argv) {
36
+ const a = { _: [] };
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const t = argv[i];
39
+ if (t === '--') { a['--'] = argv.slice(i + 1); break; } // everything after `--` is passthrough
40
+ else if (t === '--last') a.last = true;
41
+ else if (t.startsWith('--')) {
42
+ const key = t.slice(2);
43
+ const val = argv[i + 1]?.startsWith('--') || argv[i + 1] === undefined ? true : argv[++i];
44
+ a[key] = key in a ? [].concat(a[key], val) : val; // repeated flag → array
45
+ }
46
+ else a._.push(t);
47
+ }
48
+ return a;
49
+ }
50
+
51
+ function version() {
52
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
53
+ console.log(`zuzuu ${pkg.version}`);
54
+ }
55
+
56
+ function help() {
57
+ console.log(`zuzuu — evolving faculties for the coding agent you already run
58
+
59
+ usage: zuzuu <command> [options]
60
+
61
+ code [dir] launch OpenCode as the bundled default host (faculty home + capture + gate + digest)
62
+ init scaffold the faculty home (agent/) — git-style, idempotent
63
+ status detected hosts + recorded sessions
64
+ capture [--host NAME] capture a session → agent/.traces + agent/sessions.json
65
+ [--session ID] [--file PATH]
66
+ trace [--last | FILE] print a captured trace's span tree
67
+ remember "fact" [--type t] [--attr k=v] [--rel type=target]
68
+ add a knowledge item (you are the gate)
69
+ recall "query" [--type t] [--attr k=v] [--related-to id] [--semantic]
70
+ search knowledge: lexical · graph · semantic
71
+ knowledge reindex|audit rebuild the search index · check registry/items health
72
+ digest [--json] [--budget N]
73
+ print the session-start grounding brief
74
+ act [list|show <slug>|new <slug>|schema <slug>]
75
+ the Actions faculty — runbooks + runnable scripts
76
+ act <slug> [--args JSON] run a script action
77
+ act propose <slug> scaffold a proposed action → actions/inbox/ (for review)
78
+ act inbox|approve <slug>|reject <slug>
79
+ the actions gate (or use \`zuzuu review\`)
80
+ distill [--all|--session ID]
81
+ mine real sessions → knowledge proposals (default: last)
82
+ inbox what's pending your approval, per faculty
83
+ review walk pending actions + knowledge proposals (y/n/e/s/q)
84
+ proposals list|show|approve|reject <id>
85
+ the same gate, non-interactive
86
+ generation [list|show <id>|mint|rollback <id>]
87
+ pin/list/show/roll back faculty generations (lockfiles)
88
+ enable background hooks: invisible live capture + guardrails gate
89
+ disable remove the background hooks
90
+ eval [--faculty f] rank pending proposals by eval score, highest first
91
+ migrate one-time migrator: rewrite legacy candidate/er proposals to new shape
92
+ doctor environment + session health (reconciles lost sessions)
93
+ explain [topic] the 5 faculties + how graduation works
94
+ version print version
95
+ help this message
96
+
97
+ \`zuzuu capture\` works post-hoc on existing transcripts. \`zuzuu enable\` turns on
98
+ live, invisible capture across the session lifecycle — see the README.`);
99
+ }
100
+
101
+ const [cmd, ...rest] = process.argv.slice(2);
102
+ const args = parseArgs(rest);
103
+
104
+ switch (cmd) {
105
+ case 'code': process.exit(code(args)); break;
106
+ case 'init': init(args); break;
107
+ case 'remember': remember(args); break;
108
+ case 'recall': await recall(args); break;
109
+ case 'knowledge': await knowledge(args); break;
110
+ case 'digest': digest(args); break;
111
+ case 'act': act(args); break;
112
+ case 'distill': distill(args); break;
113
+ case 'inbox': inbox(args); break;
114
+ case 'review': await review(args); break;
115
+ case 'proposals': proposals(args); break;
116
+ case 'status': status(args); break;
117
+ case 'capture': capture(args); break;
118
+ case 'trace': trace(args); break;
119
+ case 'enable': enable(args); break;
120
+ case 'disable': disable(args); break;
121
+ case 'hook': runHook(args._[0], { host: args.host, session: args.session }); break;
122
+ case 'eval': evalCmd(args); break;
123
+ case 'migrate': migrate(args); break;
124
+ case 'generation': generation(args); break;
125
+ case 'doctor': await doctor(); break;
126
+ case 'explain': explain(args); break;
127
+ case 'version': case '--version': case '-v': version(); break;
128
+ case undefined: case 'help': case '--help': case '-h': help(); break;
129
+ default:
130
+ console.error(`unknown command: ${cmd}\n`);
131
+ help();
132
+ process.exit(1);
133
+ }
@@ -0,0 +1,220 @@
1
+ // Claude Code adapter — parses ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl.
2
+ //
3
+ // Richest host we have: the transcript carries tool_use blocks (stable `toolu_…`
4
+ // ids) paired to tool_result blocks (`tool_use_id` + `is_error`), each entry
5
+ // timestamped. So we build a full SESSION -> TURN -> TOOL_CALL tree with real
6
+ // durations and OK/ERROR status. No hooks, no live process — pure file parsing.
7
+
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { event, trace, EventKind, Status } from '../core/event.mjs';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const PROJECTS_DIR = join(homedir(), '.claude', 'projects');
16
+
17
+ // Claude encodes the project's cwd into the dir name by replacing non-alphanumerics with '-'.
18
+ const encodeCwd = (cwd) => cwd.replace(/[^A-Za-z0-9]/g, '-');
19
+
20
+ const ms = (iso) => (iso ? Date.parse(iso) : NaN);
21
+
22
+ function readJsonl(file) {
23
+ return readFileSync(file, 'utf8')
24
+ .split('\n')
25
+ .filter(Boolean)
26
+ .map((l) => {
27
+ try {
28
+ return JSON.parse(l);
29
+ } catch {
30
+ return null;
31
+ }
32
+ })
33
+ .filter(Boolean);
34
+ }
35
+
36
+ /** Extract plain prompt text from a user message.content (string | block array). */
37
+ function promptText(content) {
38
+ if (typeof content === 'string') return content;
39
+ if (Array.isArray(content)) {
40
+ const txt = content
41
+ .filter((b) => b.type === 'text')
42
+ .map((b) => b.text || '')
43
+ .join(' ');
44
+ return txt;
45
+ }
46
+ return '';
47
+ }
48
+
49
+ const clean = (s) => s.replace(/\s+/g, ' ').trim();
50
+ const truncate = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
51
+
52
+ export const claudeCode = {
53
+ name: 'claude-code',
54
+
55
+ detect() {
56
+ return existsSync(PROJECTS_DIR);
57
+ },
58
+
59
+ // Cross-host distill: delegate to the canonical Claude signal extractor.
60
+ // Lazy import avoids an import-time cycle (distill.mjs imports this adapter);
61
+ // mineTranscript is only called at runtime, so the cycle is harmless.
62
+ mineSignals(ref) {
63
+ try {
64
+ const file = typeof ref === 'string' ? ref : ref.ref;
65
+ // eslint-disable-next-line global-require
66
+ const { mineTranscript } = require('../../../zuzuu/knowledge/distill.mjs');
67
+ const { sessionId, ...sig } = mineTranscript(file);
68
+ return sig;
69
+ } catch {
70
+ return { commands: [], files: [], failures: [], sequences: [], correctionTurns: [], destructiveFailures: [] };
71
+ }
72
+ },
73
+
74
+ listSessions(opts = {}) {
75
+ const cwd = opts.cwd || process.cwd();
76
+ const dirs = opts.project
77
+ ? [opts.project]
78
+ : [encodeCwd(cwd)].filter((d) => existsSync(join(PROJECTS_DIR, d)));
79
+ // Fallback: if this project's dir isn't present, scan every project.
80
+ const roots = dirs.length ? dirs : (existsSync(PROJECTS_DIR) ? readdirSync(PROJECTS_DIR) : []);
81
+ const out = [];
82
+ for (const d of roots) {
83
+ const dir = join(PROJECTS_DIR, d);
84
+ if (!existsSync(dir)) continue;
85
+ for (const f of readdirSync(dir)) {
86
+ if (!f.endsWith('.jsonl')) continue;
87
+ const path = join(dir, f);
88
+ out.push({ sessionId: f.replace(/\.jsonl$/, ''), label: d, ref: path, mtime: statSync(path).mtimeMs });
89
+ }
90
+ }
91
+ return out.sort((a, b) => b.mtime - a.mtime);
92
+ },
93
+
94
+ parse(ref) {
95
+ const file = typeof ref === 'string' ? ref : ref.ref;
96
+ const rows = readJsonl(file);
97
+
98
+ let sessionId = '';
99
+ const session = { startMs: Infinity, endMs: -Infinity, model: '', version: '', cwd: '', gitBranch: '' };
100
+
101
+ // Pass 1: index tool_result end-times/status/size by tool_use_id.
102
+ const results = new Map();
103
+ for (const r of rows) {
104
+ const c = r.message?.content;
105
+ if (!Array.isArray(c)) continue;
106
+ for (const b of c) {
107
+ if (b.type !== 'tool_result') continue;
108
+ const body = typeof b.content === 'string' ? b.content : JSON.stringify(b.content ?? '');
109
+ results.set(b.tool_use_id, { endMs: ms(r.timestamp), isError: !!b.is_error, bytes: body.length });
110
+ }
111
+ }
112
+
113
+ // Pass 2: walk in order, tracking the current turn; emit turn + tool events.
114
+ const events = [];
115
+ const seenTurns = new Set();
116
+ const turnEnd = new Map(); // turnRefId -> latest child end ms
117
+ let currentTurn = null;
118
+
119
+ for (const r of rows) {
120
+ if (r.sessionId) sessionId ||= r.sessionId;
121
+ const t = ms(r.timestamp);
122
+ if (Number.isFinite(t)) {
123
+ session.startMs = Math.min(session.startMs, t);
124
+ session.endMs = Math.max(session.endMs, t);
125
+ }
126
+ if (r.cwd) session.cwd ||= r.cwd;
127
+ if (r.gitBranch) session.gitBranch ||= r.gitBranch;
128
+ if (r.version) session.version ||= r.version;
129
+ if (r.message?.model) session.model ||= r.message.model;
130
+
131
+ const role = r.message?.role;
132
+ const content = r.message?.content;
133
+
134
+ // A real user turn: role=user, has text/string content (not a tool_result), not meta.
135
+ if (r.type === 'user' && role === 'user' && !r.isMeta && Number.isFinite(t)) {
136
+ const isToolResult = Array.isArray(content) && content.some((b) => b.type === 'tool_result');
137
+ if (!isToolResult) {
138
+ const refId = r.promptId || r.uuid;
139
+ if (refId && !seenTurns.has(refId)) {
140
+ seenTurns.add(refId);
141
+ const text = clean(promptText(content));
142
+ currentTurn = refId;
143
+ events.push(
144
+ event({
145
+ kind: EventKind.TURN,
146
+ refId,
147
+ parentRefId: sessionId || 'session',
148
+ name: 'turn: ' + (truncate(text, 60) || '(empty)'),
149
+ startMs: t,
150
+ endMs: t,
151
+ attributes: { 'turn.prompt.bytes': text.length },
152
+ }),
153
+ );
154
+ }
155
+ }
156
+ }
157
+
158
+ // Tool calls: assistant content with tool_use blocks.
159
+ if (r.type === 'assistant' && Array.isArray(content)) {
160
+ for (const b of content) {
161
+ if (b.type !== 'tool_use') continue;
162
+ const res = results.get(b.id) || {};
163
+ const startMs = Number.isFinite(t) ? t : res.endMs ?? session.startMs;
164
+ const endMs = Number.isFinite(res.endMs) ? res.endMs : startMs;
165
+ const input = typeof b.input === 'string' ? b.input : JSON.stringify(b.input ?? {});
166
+ const parent = currentTurn || sessionId || 'session';
167
+ events.push(
168
+ event({
169
+ kind: EventKind.TOOL_CALL,
170
+ refId: b.id,
171
+ parentRefId: parent,
172
+ name: b.name || 'tool',
173
+ startMs,
174
+ endMs,
175
+ status: res.isError ? Status.ERROR : Status.OK,
176
+ attributes: {
177
+ 'gen_ai.operation.name': 'execute_tool',
178
+ 'gen_ai.tool.name': b.name || '',
179
+ 'host.tool.name': b.name || '',
180
+ 'tool.input.bytes': input.length, // size only — raw input is not put on the trace
181
+ 'tool.result.bytes': res.bytes ?? 0,
182
+ },
183
+ }),
184
+ );
185
+ turnEnd.set(parent, Math.max(turnEnd.get(parent) ?? 0, endMs));
186
+ }
187
+ }
188
+ }
189
+
190
+ sessionId ||= file.split('/').pop().replace(/\.jsonl$/, '');
191
+ if (!Number.isFinite(session.startMs)) session.startMs = 0;
192
+ if (!Number.isFinite(session.endMs)) session.endMs = session.startMs;
193
+
194
+ // Extend each turn to cover its tool children.
195
+ for (const e of events) {
196
+ if (e.kind === EventKind.TURN && turnEnd.has(e.refId)) e.endMs = Math.max(e.endMs, turnEnd.get(e.refId));
197
+ }
198
+
199
+ // The SESSION root.
200
+ events.unshift(
201
+ event({
202
+ kind: EventKind.SESSION,
203
+ refId: sessionId,
204
+ parentRefId: null,
205
+ name: `session ${sessionId.slice(0, 8)} (claude-code)`,
206
+ startMs: session.startMs,
207
+ endMs: session.endMs,
208
+ attributes: {
209
+ 'host.name': 'claude-code',
210
+ 'host.session.model': session.model,
211
+ 'host.session.version': session.version,
212
+ 'host.cwd': session.cwd,
213
+ 'host.git.branch': session.gitBranch,
214
+ },
215
+ }),
216
+ );
217
+
218
+ return trace({ host: 'claude-code', sessionId, title: session.cwd, events });
219
+ },
220
+ };
@@ -0,0 +1,201 @@
1
+ // Codex CLI adapter — parses ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl.
2
+ //
3
+ // Built against REAL wire data (a captured `codex exec` session), not docs —
4
+ // the docs warn the serialization differs from source. Confirmed shapes:
5
+ // { timestamp, type, payload } per line
6
+ // type "session_meta" → payload { id, cwd, ... } (the session id)
7
+ // type "turn_context" → payload { model, cwd }
8
+ // type "event_msg" → payload { type: task_started | user_message | agent_message | token_count | task_complete, message? }
9
+ // type "response_item" → payload { type: "message"|"function_call"|"function_call_output", ... }
10
+ // message → { role: developer|user|assistant, content:[{type,text}] }
11
+ // function_call → { name, call_id, arguments(JSON string) }
12
+ // function_call_output → { call_id, output } (linked FLAT by call_id)
13
+ //
14
+ // Turns come from event_msg/user_message (clean prompt text — avoids the injected
15
+ // <environment_context>/developer messages). Tool spans pair function_call ↔
16
+ // function_call_output by call_id, giving real durations. Rich, like Claude.
17
+
18
+ import { homedir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
21
+ import { event, trace, EventKind, Status } from '../core/event.mjs';
22
+ import { assembleSignals, emptySignals } from './signals.mjs';
23
+
24
+ const SESSIONS_DIR = join(homedir(), '.codex', 'sessions');
25
+
26
+ // Codex shell tool (real-wire): function_call name "exec_command", arguments is a
27
+ // JSON string {cmd:"…"} (older/other builds use "shell" with {command:[…]}). The
28
+ // paired function_call_output carries no error flag — but its text begins with
29
+ // "Process exited with code N", so N≠0 ⇒ failed.
30
+ const CODEX_SHELL = new Set(['exec_command', 'shell', 'local_shell', 'bash']);
31
+ function codexCmdText(args) {
32
+ const a = typeof args === 'string' ? (() => { try { return JSON.parse(args); } catch { return {}; } })() : args ?? {};
33
+ if (typeof a.cmd === 'string') return a.cmd;
34
+ if (typeof a.command === 'string') return a.command;
35
+ if (Array.isArray(a.command)) return a.command.join(' ');
36
+ return '';
37
+ }
38
+ const codexFailed = (output) => /Process exited with code\s+([0-9]+)/i.test(String(output || '')) && !/Process exited with code\s+0\b/i.test(String(output || ''));
39
+ const ms = (iso) => (iso ? Date.parse(iso) : NaN);
40
+ const clean = (s) => String(s).replace(/\s+/g, ' ').trim();
41
+ const truncate = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
42
+
43
+ function readJsonl(file) {
44
+ return readFileSync(file, 'utf8')
45
+ .split('\n')
46
+ .filter(Boolean)
47
+ .map((l) => {
48
+ try {
49
+ return JSON.parse(l);
50
+ } catch {
51
+ return null;
52
+ }
53
+ })
54
+ .filter(Boolean);
55
+ }
56
+
57
+ export const codex = {
58
+ name: 'codex',
59
+
60
+ detect() {
61
+ return existsSync(SESSIONS_DIR);
62
+ },
63
+
64
+ listSessions() {
65
+ if (!existsSync(SESSIONS_DIR)) return [];
66
+ return readdirSync(SESSIONS_DIR, { recursive: true })
67
+ .filter((f) => typeof f === 'string' && /rollout-.*\.jsonl$/.test(f))
68
+ .map((f) => {
69
+ const path = join(SESSIONS_DIR, f);
70
+ const m = f.match(/rollout-.*-([0-9a-f-]{36})\.jsonl$/i);
71
+ return { sessionId: m ? m[1] : f, ref: path, label: 'codex', mtime: statSync(path).mtimeMs };
72
+ })
73
+ .sort((a, b) => b.mtime - a.mtime);
74
+ },
75
+
76
+ // Cross-host distill: shell command TEXT + failed flag from the raw rollout.
77
+ mineSignals(ref) {
78
+ try {
79
+ const file = typeof ref === 'string' ? ref : ref.ref;
80
+ const rows = readJsonl(file);
81
+ const outputs = new Map(); // call_id -> output text
82
+ for (const r of rows) {
83
+ const p = r.payload || {};
84
+ if (r.type === 'response_item' && p.type === 'function_call_output') outputs.set(p.call_id, p.output);
85
+ }
86
+ const shellCalls = [];
87
+ for (const r of rows) {
88
+ const p = r.payload || {};
89
+ if (r.type === 'response_item' && p.type === 'function_call' && CODEX_SHELL.has(p.name)) {
90
+ const cmd = codexCmdText(p.arguments);
91
+ if (cmd) shellCalls.push({ cmd, failed: codexFailed(outputs.get(p.call_id)), tool: p.name });
92
+ }
93
+ }
94
+ return assembleSignals(shellCalls);
95
+ } catch {
96
+ return emptySignals();
97
+ }
98
+ },
99
+
100
+ parse(ref) {
101
+ const file = typeof ref === 'string' ? ref : ref.ref;
102
+ const rows = readJsonl(file);
103
+
104
+ let sessionId = '';
105
+ const meta = { startMs: Infinity, endMs: -Infinity, model: '', cwd: '' };
106
+
107
+ // Pass 1: index function_call_output by call_id (end time + size).
108
+ const results = new Map();
109
+ for (const r of rows) {
110
+ const p = r.payload || {};
111
+ if (r.type === 'response_item' && p.type === 'function_call_output') {
112
+ const out = typeof p.output === 'string' ? p.output : JSON.stringify(p.output ?? '');
113
+ results.set(p.call_id, { endMs: ms(r.timestamp), bytes: out.length });
114
+ }
115
+ }
116
+
117
+ // Pass 2: walk in order; turns from user_message, tools from function_call.
118
+ const events = [];
119
+ const turnEnd = new Map();
120
+ let currentTurn = null;
121
+ let turnIdx = 0;
122
+
123
+ for (const r of rows) {
124
+ const p = r.payload || {};
125
+ const t = ms(r.timestamp);
126
+ if (Number.isFinite(t)) {
127
+ meta.startMs = Math.min(meta.startMs, t);
128
+ meta.endMs = Math.max(meta.endMs, t);
129
+ }
130
+ if (r.type === 'session_meta') sessionId ||= p.id || '';
131
+ if (r.type === 'session_meta' || r.type === 'turn_context') meta.cwd ||= p.cwd || '';
132
+ if (r.type === 'turn_context') meta.model ||= p.model || '';
133
+
134
+ if (r.type === 'event_msg' && p.type === 'user_message') {
135
+ const text = clean(p.message || '');
136
+ const refId = `${sessionId || 'codex'}:turn:${turnIdx++}`;
137
+ currentTurn = refId;
138
+ events.push(
139
+ event({
140
+ kind: EventKind.TURN,
141
+ refId,
142
+ parentRefId: sessionId || 'session',
143
+ name: 'turn: ' + (truncate(text, 60) || '(empty)'),
144
+ startMs: Number.isFinite(t) ? t : meta.startMs,
145
+ endMs: Number.isFinite(t) ? t : meta.startMs,
146
+ attributes: { 'turn.prompt.bytes': text.length },
147
+ }),
148
+ );
149
+ }
150
+
151
+ if (r.type === 'response_item' && p.type === 'function_call') {
152
+ const res = results.get(p.call_id) || {};
153
+ const startMs = Number.isFinite(t) ? t : meta.startMs;
154
+ const endMs = Number.isFinite(res.endMs) ? res.endMs : startMs;
155
+ const args = typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments ?? {});
156
+ const parent = currentTurn || sessionId || 'session';
157
+ events.push(
158
+ event({
159
+ kind: EventKind.TOOL_CALL,
160
+ refId: p.call_id || `${sessionId}:call:${events.length}`,
161
+ parentRefId: parent,
162
+ name: p.name || 'tool',
163
+ startMs,
164
+ endMs,
165
+ status: Status.OK, // Codex output carries no explicit error flag (see CONCLUSIONS)
166
+ attributes: {
167
+ 'gen_ai.operation.name': 'execute_tool',
168
+ 'gen_ai.tool.name': p.name || '',
169
+ 'host.tool.name': p.name || '',
170
+ 'tool.input.bytes': args.length,
171
+ 'tool.result.bytes': res.bytes ?? 0,
172
+ },
173
+ }),
174
+ );
175
+ turnEnd.set(parent, Math.max(turnEnd.get(parent) ?? 0, endMs));
176
+ }
177
+ }
178
+
179
+ sessionId ||= (file.match(/([0-9a-f-]{36})\.jsonl$/i) || [])[1] || file.split('/').pop();
180
+ if (!Number.isFinite(meta.startMs)) meta.startMs = 0;
181
+ if (!Number.isFinite(meta.endMs)) meta.endMs = meta.startMs;
182
+
183
+ for (const e of events) {
184
+ if (e.kind === EventKind.TURN && turnEnd.has(e.refId)) e.endMs = Math.max(e.endMs, turnEnd.get(e.refId));
185
+ }
186
+
187
+ events.unshift(
188
+ event({
189
+ kind: EventKind.SESSION,
190
+ refId: sessionId,
191
+ parentRefId: null,
192
+ name: `session ${String(sessionId).slice(0, 8)} (codex)`,
193
+ startMs: meta.startMs,
194
+ endMs: meta.endMs,
195
+ attributes: { 'host.name': 'codex', 'host.session.model': meta.model, 'host.cwd': meta.cwd },
196
+ }),
197
+ );
198
+
199
+ return trace({ host: 'codex', sessionId: String(sessionId), title: meta.cwd, events });
200
+ },
201
+ };