@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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/bin/zuzuu.mjs +133 -0
- package/experiments/experiment-1-trace-capture/adapters/claude-code.mjs +220 -0
- package/experiments/experiment-1-trace-capture/adapters/codex.mjs +201 -0
- package/experiments/experiment-1-trace-capture/adapters/gemini-cli.mjs +113 -0
- package/experiments/experiment-1-trace-capture/adapters/host-adapter.mjs +43 -0
- package/experiments/experiment-1-trace-capture/adapters/opencode.mjs +205 -0
- package/experiments/experiment-1-trace-capture/adapters/pi.mjs +218 -0
- package/experiments/experiment-1-trace-capture/adapters/registry.mjs +20 -0
- package/experiments/experiment-1-trace-capture/adapters/signals.mjs +44 -0
- package/experiments/experiment-1-trace-capture/core/event.mjs +58 -0
- package/experiments/experiment-1-trace-capture/core/ids.mjs +32 -0
- package/experiments/experiment-1-trace-capture/core/otlp.mjs +54 -0
- package/experiments/experiment-1-trace-capture/core/render.mjs +63 -0
- package/experiments/experiment-1-trace-capture/core/spans.mjs +43 -0
- package/package.json +56 -0
- package/zuzuu/actions/adapter.mjs +130 -0
- package/zuzuu/actions/convert.mjs +27 -0
- package/zuzuu/actions/dispatch.mjs +87 -0
- package/zuzuu/actions/inbox.mjs +56 -0
- package/zuzuu/actions/manifest.mjs +72 -0
- package/zuzuu/actions/marker.mjs +4 -0
- package/zuzuu/actions/runner.mjs +37 -0
- package/zuzuu/actions/schema.mjs +73 -0
- package/zuzuu/actions/trail.mjs +22 -0
- package/zuzuu/capture-core.mjs +49 -0
- package/zuzuu/commands/act-author.mjs +72 -0
- package/zuzuu/commands/act.mjs +101 -0
- package/zuzuu/commands/capture.mjs +32 -0
- package/zuzuu/commands/code.mjs +84 -0
- package/zuzuu/commands/digest.mjs +23 -0
- package/zuzuu/commands/distill.mjs +46 -0
- package/zuzuu/commands/doctor.mjs +197 -0
- package/zuzuu/commands/enable.mjs +195 -0
- package/zuzuu/commands/eval.mjs +101 -0
- package/zuzuu/commands/explain.mjs +119 -0
- package/zuzuu/commands/generation.mjs +107 -0
- package/zuzuu/commands/hook.mjs +209 -0
- package/zuzuu/commands/inbox.mjs +73 -0
- package/zuzuu/commands/init.mjs +89 -0
- package/zuzuu/commands/knowledge.mjs +152 -0
- package/zuzuu/commands/migrate.mjs +125 -0
- package/zuzuu/commands/review.mjs +299 -0
- package/zuzuu/commands/status.mjs +82 -0
- package/zuzuu/commands/trace.mjs +19 -0
- package/zuzuu/digest.mjs +149 -0
- package/zuzuu/eval/rank.mjs +31 -0
- package/zuzuu/eval/score.mjs +85 -0
- package/zuzuu/eval/signals.mjs +57 -0
- package/zuzuu/faculty/contract.mjs +19 -0
- package/zuzuu/faculty/gate.mjs +65 -0
- package/zuzuu/faculty/generation.mjs +392 -0
- package/zuzuu/faculty/proposal.mjs +166 -0
- package/zuzuu/faculty/provenance.mjs +35 -0
- package/zuzuu/faculty/registry.mjs +33 -0
- package/zuzuu/faculty/trail.mjs +27 -0
- package/zuzuu/guardrails/adapter.mjs +134 -0
- package/zuzuu/guardrails.mjs +89 -0
- package/zuzuu/inject.mjs +46 -0
- package/zuzuu/instructions/adapter.mjs +93 -0
- package/zuzuu/knowledge/adapter.mjs +99 -0
- package/zuzuu/knowledge/distill.mjs +237 -0
- package/zuzuu/knowledge/embed.mjs +52 -0
- package/zuzuu/knowledge/er.mjs +98 -0
- package/zuzuu/knowledge/inbox.mjs +43 -0
- package/zuzuu/knowledge/index.mjs +194 -0
- package/zuzuu/knowledge/items.mjs +154 -0
- package/zuzuu/knowledge/proposals.mjs +196 -0
- package/zuzuu/knowledge/registry.mjs +115 -0
- package/zuzuu/live/install.mjs +76 -0
- package/zuzuu/live/live-store.mjs +78 -0
- package/zuzuu/live/probe.mjs +55 -0
- package/zuzuu/live/reconcile.mjs +33 -0
- package/zuzuu/memory/adapter.mjs +121 -0
- package/zuzuu/miners/actions.mjs +118 -0
- package/zuzuu/miners/guardrails.mjs +174 -0
- package/zuzuu/miners/instructions.mjs +152 -0
- package/zuzuu/miners/knowledge.mjs +22 -0
- package/zuzuu/miners/memory.mjs +27 -0
- package/zuzuu/miners/registry.mjs +31 -0
- package/zuzuu/scaffold.mjs +213 -0
- package/zuzuu/session.mjs +72 -0
- 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
|
+
[](https://github.com/h1902y/zuzuu/actions/workflows/ci.yml) [](https://www.npmjs.com/package/@zuzuucodes/cli) [](package.json) [](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
|
+
};
|