parawatch 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 penspanic
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 CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ # parawatch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A control tower for people who run many AI coding agents at once, across several projects and several tasks inside each one.
4
+
5
+ <picture>
6
+ <source media="(prefers-color-scheme: dark)" srcset="docs/app-dark.png">
7
+ <img src="docs/app-light.png" alt="The parawatch desktop app: a chart of the last hour of agent activity per project, then the cards that need you, one with two screenshots and three options." width="560">
8
+ </picture>
9
+
10
+ It answers three questions:
11
+
12
+ - **What needs me?** Decisions to make, builds or videos to try, things only you can do.
13
+ - **What is running?** Every agent session, its subagents and its `codex exec` jobs, grouped by project, with what each one is doing right now.
14
+ - **Did my answer reach the agent?** When you answer a card, the agent waiting on it wakes up. You do not go back to the chat to say "I decided".
15
+
16
+ parawatch is a CLI first. Agents use the CLI, and so can you. There is no server and nothing leaves your machine. On top of the CLI, this repository has a [desktop app](apps/desktop/README.md) (the panel above, a menu bar count, a Dock badge and notifications).
17
+
18
+ ```
19
+ $ parawatch status
20
+ NEEDS YOU 3
21
+ c1 decide game/139 Which ending for the shake box? 12m
22
+ c2 try game/139 Try build 139: does the glass read as glass? 4m
23
+ c3 blocked app Install the iPad simulator runtime 1h
24
+
25
+ AGENTS 5 working
26
+ app
27
+ ● Interval lesson screen 1m Bash: Build the app
28
+ ○ App Director 24m PR 18 passed. Waiting for the simulator runtime.
29
+ game
30
+ ● Game Director 1s Bash: Wait for answers
31
+ ├ ● 139 shake round 2 1s Bash: Run tests
32
+ ├ ● Generate the level icons 2m exec: python3 make_icons.py
33
+ └ ● Glass shader pass 9m Bash: Capture the tank
34
+ ```
35
+
36
+ One line per item, always inside the terminal width. Finished subagents fold into a count after five minutes (`parawatch runs --all` shows them). `--wide` adds the runtime and ids. Colors follow `NO_COLOR` and `FORCE_COLOR`.
37
+
38
+ ## Install
39
+
40
+ There are three parts. Most people want all three.
41
+
42
+ **The CLI**, which agents and you use:
43
+
44
+ ```
45
+ npm install -g parawatch
46
+ ```
47
+
48
+ **The agent skill**, so Claude Code and Codex agents learn the protocol on their own:
49
+
50
+ ```
51
+ parawatch skill install
52
+ ```
53
+
54
+ It links [`skills/parawatch`](skills/parawatch/SKILL.md) into `~/.claude/skills` and `~/.codex/skills`. The installed skill is a link to the copy in the package, so updating parawatch updates the skill.
55
+
56
+ **The desktop app**, to see and answer cards without a terminal. On macOS, download the DMG from the [latest release](https://github.com/penspanic/parawatch/releases/latest): `arm64` for Apple silicon, `x64` for Intel. It is signed and notarized, so it opens without warnings. Elsewhere, [build it from source](apps/desktop/README.md#run-from-source).
57
+
58
+ ### Platforms
59
+
60
+ | | macOS | Linux | Windows |
61
+ |--|--|--|--|
62
+ | CLI | used daily | tests pass in CI | not supported yet |
63
+ | Desktop app | used daily | untested | untested |
64
+ | Agent logs read | Claude Code, Codex | same | |
65
+
66
+ Node 20 or newer.
67
+
68
+ ## Concepts
69
+
70
+ | | |
71
+ |--|--|
72
+ | **Project** | A name and one or more paths. Git worktrees of a path belong to the project too. |
73
+ | **Card** | Something that needs the human. Four kinds: `decide` (pick an option or write an answer), `try` (open a build, watch a video, look at a screen), `block` (only the human can do it and the agent is stuck until they do: install, sudo, sign in), `todo` (only the human can do it, but nothing waits on it now: check a bill, register a company). A card remembers the session that opened it, and `--since` keeps when the matter first came up when it is older than the card. |
74
+ | **Run** | An agent at work. Most runs are *observed*: parawatch reads the agent's own log. Jobs that leave no log (a bake, a benchmark) can be *declared* with `parawatch run start`. |
75
+ | **Lead** | A role, not a concept of its own. Claim a session with `--role lead` and it shows at the top of its project with its children under it. |
76
+
77
+ ## For the human
78
+
79
+ ```
80
+ parawatch status # what needs you, then what is running
81
+ parawatch cards # open and answered cards
82
+ parawatch show c4 # one card with its body, options and attachments
83
+ parawatch answer c4 -c 2 # pick option 2
84
+ parawatch answer c4 -m "fill it to the rim" # or just say something
85
+ parawatch answer c5 -m "glass is too dark" --attach ~/Desktop/shot.png
86
+ parawatch runs --since 12 # agents seen in the last 12 hours
87
+ parawatch runs -p game --all # include finished ones
88
+ parawatch runs --wide # with runtime and ids
89
+ ```
90
+
91
+ Register projects once:
92
+
93
+ ```
94
+ parawatch project add game ~/dev/game
95
+ parawatch project add app ~/dev/app ~/notes/app
96
+ parawatch claim 811a9612 -p game --role lead --title "Game Director"
97
+ ```
98
+
99
+ `claim` is for sessions that do not start inside a project path (an agent run from a notes folder, a scratch directory). Children of a claimed session follow it.
100
+
101
+ ## For agents
102
+
103
+ Add the agent protocol to a project's instruction file:
104
+
105
+ ```
106
+ parawatch init -f AGENTS.md -p game # or CLAUDE.md
107
+ ```
108
+
109
+ It writes a short block between `<!-- parawatch:begin -->` and `<!-- parawatch:end -->` and replaces it in place on later runs. The protocol in short:
110
+
111
+ ```
112
+ parawatch ask -p game "Which ending?" -o "Side tank" -o "Top box" -r 2 -b "two or three lines"
113
+ parawatch try -p game "Try build 139" --open /path/Game.app --look "does the glass read?"
114
+ parawatch block -p app "Install the iPad simulator runtime"
115
+ parawatch todo -p game "Check the GitHub bill" --since 2026-10-01
116
+ parawatch wait -p game # run in the background; exits when the human answers
117
+ parawatch comment c4 "baking the top box now" # a note on the card; does not wake anyone
118
+ parawatch close c4 -n "used the top box"
119
+ ```
120
+
121
+ `parawatch edit <card>` changes a card after the fact: its kind, wording, task, who asked or `--since`. `parawatch goto <card>` opens the session that asked in the Claude desktop app (`--copy` also puts a line about the card on the clipboard, to paste there); sessions outside the desktop app get the `claude --resume` command instead.
122
+
123
+ `wait` prints the answers and exits with code 0. With `--timeout <seconds>` it exits with code 2 when nothing came. An answer given before `wait` starts is not lost: each consumer has its own cursor, and `wait` returns everything after it.
124
+
125
+ ### Agent skill
126
+
127
+ [`skills/parawatch/SKILL.md`](skills/parawatch/SKILL.md) teaches an agent the whole protocol: claiming its session, writing a good card, keeping one background wait armed, closing cards, declaring long jobs, and who asks the human when a lead runs workers. Install it with `parawatch skill install` (see [Install](#install)); `parawatch skill path` prints where it lives, for runtimes that load skills from elsewhere.
128
+
129
+ `claim`, `run start` and every card command pick up the calling session from `$CLAUDE_CODE_SESSION_ID` (or `$CODEX_THREAD_ID`), so an agent can put itself on the board without knowing its id.
130
+
131
+ ### Waking a Claude Code session
132
+
133
+ Claude Code re-invokes a session when one of its background commands exits. So an agent that runs `parawatch wait -p <project>` as a background command is woken by your answer, with the answer as the command's output.
134
+
135
+ As a backstop, the hook adds any unhandled answers to the next turn of a session in that project:
136
+
137
+ ```json
138
+ {
139
+ "hooks": {
140
+ "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "parawatch hook claude-code" }] }],
141
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "parawatch hook claude-code" }] }]
142
+ }
143
+ }
144
+ ```
145
+
146
+ The hook only peeks. The agent's own `wait` or `inbox` moves its cursor.
147
+
148
+ ### Other runtimes
149
+
150
+ Anything that can run a command and notice when it exits can use `wait`. `parawatch inbox` returns at once (add `--peek` to leave the cursor where it is), for runtimes that poll.
151
+
152
+ ## Observed agents
153
+
154
+ parawatch reads agent logs. It never writes to them and never sends them anywhere.
155
+
156
+ | Runtime | Where it reads | What it shows |
157
+ |--|--|--|
158
+ | Claude Code | `~/.claude/projects/*/<session>.jsonl` and `<session>/subagents/` (or `$CLAUDE_CONFIG_DIR`) | sessions with their title, subagents with their description and model, the tool in flight |
159
+ | Codex | `~/.codex/sessions/` (or `$CODEX_HOME`) | `codex exec` jobs and interactive sessions with their prompt and last tool call; jobs started from a Claude Code session show under it |
160
+
161
+ States: `●` working, `◐` waiting, `○` idle, `✓` done, `!` stalled (quiet longer than its tool's timeout allows), `✗` failed.
162
+
163
+ Runs that match no registered project show under `other`. A run belongs to a project by, in order: a claim, its working directory, its parent's project, a project path named in its first prompt.
164
+
165
+ ### Adding a runtime
166
+
167
+ An adapter is one file in `src/adapters/` that implements:
168
+
169
+ ```ts
170
+ interface Adapter {
171
+ name: string;
172
+ scan(opts: { sinceMs: number; now: number }): Run[];
173
+ }
174
+ ```
175
+
176
+ It reads only, reads only files touched since `sinceMs`, and reads only the head and tail of large logs.
177
+
178
+ ## Apps, plugins and dashboards
179
+
180
+ | | |
181
+ |--|--|
182
+ | [`apps/desktop`](apps/desktop/README.md) | The desktop app. Reads and writes the store directly, so it works without the CLI on `PATH`. |
183
+ | [`packages/panel`](packages/panel) | The panel both of them show, as plain DOM, with a harness page for working on it. |
184
+
185
+ Anything else can build on the same two commands:
186
+
187
+ ```
188
+ parawatch status --json # one snapshot: projects, cards, runs
189
+ parawatch watch # every new event as one JSON line
190
+ parawatch watch --from 120 # starting after event 120
191
+ ```
192
+
193
+ ## Storage
194
+
195
+ Everything lives in one append-only file, `~/.local/state/parawatch/events.jsonl` (`$PARAWATCH_HOME`, or `$XDG_STATE_HOME/parawatch`). Writers take a short lock, so several agents can write at once. Cursors live next to it in `cursors/`. Delete the folder to start over.
196
+
197
+ Desktop notifications for new cards use `osascript` on macOS and `notify-send` on Linux. `PARAWATCH_NOTIFY=0` turns them off. While the desktop app runs it notifies instead, and the CLI stays quiet. To-dos never notify.
198
+
199
+ ## Not goals
200
+
201
+ - Starting or steering agents. Use whatever you already use; parawatch only watches and carries answers back.
202
+ - A shared team tracker. It is for one person and their agents, on one machine.
203
+ - Replacing your issue tracker. Cards are short-lived questions, not the backlog.
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,226 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { isObj, logActivity, oneLine, readHeadLines, readTailLines, str } from "./jsonl.js";
5
+ const HEAD_BYTES = 96 * 1024;
6
+ const TAIL_BYTES = 384 * 1024;
7
+ export function claudeProjectsDir() {
8
+ if (process.env.PARAWATCH_CLAUDE_DIR)
9
+ return process.env.PARAWATCH_CLAUDE_DIR;
10
+ const base = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
11
+ return path.join(base, "projects");
12
+ }
13
+ function blocks(rec) {
14
+ const msg = rec.message;
15
+ if (!isObj(msg))
16
+ return [];
17
+ const c = msg.content;
18
+ if (typeof c === "string")
19
+ return [{ type: "text", text: c }];
20
+ return Array.isArray(c) ? c.filter(isObj) : [];
21
+ }
22
+ function isConversation(rec) {
23
+ return (rec.type === "user" || rec.type === "assistant") && rec.isMeta !== true;
24
+ }
25
+ function toolSummary(b) {
26
+ const inp = b.input ?? {};
27
+ const pick = str(inp.description) || str(inp.command) || str(inp.file_path) || str(inp.pattern) || str(inp.prompt) || str(inp.url);
28
+ const short = pick && (inp.file_path === pick ? path.basename(pick) : pick);
29
+ return short ? `${b.name}: ${oneLine(short, 80)}` : String(b.name ?? "tool");
30
+ }
31
+ /** A prompt the human (or parent agent) typed, not a tool result or system notice. */
32
+ function promptText(rec) {
33
+ if (rec.type !== "user" || rec.isMeta === true)
34
+ return undefined;
35
+ const bs = blocks(rec);
36
+ if (bs.some((b) => b.type === "tool_result"))
37
+ return undefined;
38
+ const text = bs.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
39
+ if (!text || text.startsWith("<"))
40
+ return undefined;
41
+ return text;
42
+ }
43
+ /** Work out what a Claude Code transcript is doing from its last records. */
44
+ export function readClaudeTail(recs, kind) {
45
+ const out = { state: "unknown" };
46
+ let lastConv;
47
+ for (const r of recs) {
48
+ if (!isObj(r))
49
+ continue;
50
+ const ts = str(r.timestamp);
51
+ if (ts && (!out.lastActivity || ts > out.lastActivity))
52
+ out.lastActivity = ts;
53
+ if (str(r.cwd))
54
+ out.cwd = str(r.cwd);
55
+ if (r.type === "custom-title" && str(r.customTitle))
56
+ out.title = str(r.customTitle);
57
+ if (r.type === "agent-name" && str(r.agentName) && !out.title)
58
+ out.title = str(r.agentName);
59
+ if (isConversation(r))
60
+ lastConv = r;
61
+ }
62
+ if (!lastConv)
63
+ return out;
64
+ const bs = blocks(lastConv);
65
+ if (lastConv.type === "assistant") {
66
+ const tool = [...bs].reverse().find((b) => b.type === "tool_use");
67
+ if (tool) {
68
+ out.state = "working";
69
+ out.action = toolSummary(tool);
70
+ const t = tool.input?.timeout;
71
+ if (typeof t === "number")
72
+ out.pendingTimeoutMs = t;
73
+ }
74
+ else if (bs.some((b) => b.type === "text" && b.text?.trim())) {
75
+ out.state = kind === "subagent" ? "done" : "idle";
76
+ const text = bs.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ");
77
+ out.action = oneLine(text, 100);
78
+ }
79
+ else {
80
+ out.state = "working";
81
+ out.action = "thinking";
82
+ }
83
+ }
84
+ else {
85
+ out.state = "working";
86
+ const result = bs.find((b) => b.type === "tool_result");
87
+ out.action = result ? "thinking" : "reading a new message";
88
+ }
89
+ return out;
90
+ }
91
+ function firstPrompt(file) {
92
+ const out = {};
93
+ for (const r of readHeadLines(file, HEAD_BYTES)) {
94
+ if (!isObj(r))
95
+ continue;
96
+ if (!out.startedAt && str(r.timestamp))
97
+ out.startedAt = str(r.timestamp);
98
+ if (!out.cwd && str(r.cwd))
99
+ out.cwd = str(r.cwd);
100
+ if (!out.prompt)
101
+ out.prompt = promptText(r);
102
+ if (out.prompt && out.cwd && out.startedAt)
103
+ break;
104
+ }
105
+ return out;
106
+ }
107
+ function readMeta(file) {
108
+ try {
109
+ const data = JSON.parse(fs.readFileSync(file.replace(/\.jsonl$/, ".meta.json"), "utf8"));
110
+ return isObj(data) ? data : {};
111
+ }
112
+ catch {
113
+ return {};
114
+ }
115
+ }
116
+ const STALL_MS = 20 * 60_000;
117
+ export function applyStall(run, pendingTimeoutMs, now) {
118
+ if (run.state !== "working" || !run.lastActivity)
119
+ return;
120
+ const quiet = now - Date.parse(run.lastActivity);
121
+ const allowed = Math.max(STALL_MS, (pendingTimeoutMs ?? 0) + 2 * 60_000);
122
+ if (quiet > allowed)
123
+ run.state = "stalled";
124
+ }
125
+ function listJsonl(dir) {
126
+ try {
127
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path.join(dir, f));
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
133
+ function fresh(file, sinceMs) {
134
+ try {
135
+ return fs.statSync(file).mtimeMs >= sinceMs;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ export const claudeCode = {
142
+ name: "claude-code",
143
+ scan(opts) {
144
+ const root = claudeProjectsDir();
145
+ let dirs;
146
+ try {
147
+ dirs = fs.readdirSync(root).map((d) => path.join(root, d));
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ const runs = [];
153
+ for (const dir of dirs) {
154
+ for (const file of listJsonl(dir)) {
155
+ if (fresh(file, opts.sinceMs))
156
+ runs.push(readSession(file, opts.now));
157
+ }
158
+ let subdirs = [];
159
+ try {
160
+ subdirs = fs.readdirSync(dir).map((d) => path.join(dir, d, "subagents"));
161
+ }
162
+ catch {
163
+ // not a directory listing we can read
164
+ }
165
+ for (const sub of subdirs) {
166
+ for (const file of listJsonl(sub)) {
167
+ if (fresh(file, opts.sinceMs))
168
+ runs.push(readSubagent(file, opts.now));
169
+ }
170
+ }
171
+ }
172
+ return runs;
173
+ },
174
+ };
175
+ function readSession(file, now) {
176
+ const id = path.basename(file, ".jsonl");
177
+ const base = { id, runtime: "claude-code", kind: "session", title: id.slice(0, 8), state: "unknown", source: file };
178
+ try {
179
+ const head = firstPrompt(file);
180
+ const tail = readClaudeTail(readTailLines(file, TAIL_BYTES), "session");
181
+ const run = {
182
+ ...base,
183
+ title: tail.title || (head.prompt ? oneLine(head.prompt, 60) : base.title),
184
+ cwd: tail.cwd || head.cwd,
185
+ startedAt: head.startedAt,
186
+ lastActivity: tail.lastActivity,
187
+ state: tail.state,
188
+ action: tail.action,
189
+ prompt: head.prompt ? oneLine(head.prompt, 400) : undefined,
190
+ activity: logActivity(file, now),
191
+ };
192
+ applyStall(run, tail.pendingTimeoutMs, now);
193
+ return run;
194
+ }
195
+ catch (err) {
196
+ return { ...base, error: `could not read: ${err.message}` };
197
+ }
198
+ }
199
+ function readSubagent(file, now) {
200
+ const id = path.basename(file, ".jsonl").replace(/^agent-/, "");
201
+ const parent = path.basename(path.dirname(path.dirname(file)));
202
+ const base = { id, runtime: "claude-code", kind: "subagent", parent, title: id.slice(0, 8), state: "unknown", source: file };
203
+ try {
204
+ const meta = readMeta(file);
205
+ const head = firstPrompt(file);
206
+ const tail = readClaudeTail(readTailLines(file, TAIL_BYTES), "subagent");
207
+ const run = {
208
+ ...base,
209
+ title: str(meta.description) || (head.prompt ? oneLine(head.prompt, 60) : base.title),
210
+ model: str(meta.model),
211
+ cwd: tail.cwd || head.cwd,
212
+ startedAt: head.startedAt,
213
+ lastActivity: tail.lastActivity,
214
+ state: tail.state,
215
+ action: tail.action,
216
+ prompt: head.prompt ? oneLine(head.prompt, 400) : undefined,
217
+ activity: logActivity(file, now),
218
+ };
219
+ applyStall(run, tail.pendingTimeoutMs, now);
220
+ return run;
221
+ }
222
+ catch (err) {
223
+ return { ...base, error: `could not read: ${err.message}` };
224
+ }
225
+ }
226
+ //# sourceMappingURL=claudeCode.js.map
@@ -0,0 +1,185 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { applyStall } from "./claudeCode.js";
5
+ import { isObj, logActivity, oneLine, readHeadLines, readHeadText, readTailLines, str, textValues } from "./jsonl.js";
6
+ const HEAD_BYTES = 128 * 1024;
7
+ const TAIL_BYTES = 256 * 1024;
8
+ // prompts can sit behind several inline base64 images on one line; read this far only when needed
9
+ const RAW_HEAD_BYTES = 16 * 1024 * 1024;
10
+ export function codexSessionsDir() {
11
+ if (process.env.PARAWATCH_CODEX_DIR)
12
+ return process.env.PARAWATCH_CODEX_DIR;
13
+ const base = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
14
+ return path.join(base, "sessions");
15
+ }
16
+ const PARENT_IN_CWD = /\/claude-\d+\/[^/]+\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\/|$)/;
17
+ /** A Claude Code session id when Codex was started from inside that session's scratch space. */
18
+ export function parentFromCwd(cwd) {
19
+ return cwd ? PARENT_IN_CWD.exec(cwd)?.[1] : undefined;
20
+ }
21
+ function payload(r) {
22
+ return isObj(r) && isObj(r.payload) ? r.payload : undefined;
23
+ }
24
+ function messageText(p) {
25
+ const c = p.content;
26
+ if (!Array.isArray(c))
27
+ return "";
28
+ return c
29
+ .filter(isObj)
30
+ .map((b) => str(b.text) ?? "")
31
+ .join("\n")
32
+ .trim();
33
+ }
34
+ function promptBlock(p) {
35
+ const c = p.content;
36
+ if (!Array.isArray(c))
37
+ return undefined;
38
+ for (const b of c.filter(isObj)) {
39
+ const t = (str(b.text) ?? "").trim();
40
+ if (!t || t.startsWith("<") || t.startsWith("# AGENTS.md instructions"))
41
+ continue;
42
+ return t;
43
+ }
44
+ return undefined;
45
+ }
46
+ function toolSummary(p) {
47
+ const name = str(p.name) ?? "tool";
48
+ let arg = str(p.input) ?? str(p.arguments) ?? "";
49
+ try {
50
+ const parsed = JSON.parse(arg);
51
+ if (isObj(parsed))
52
+ arg = str(parsed.cmd) ?? str(parsed.command) ?? (Array.isArray(parsed.command) ? parsed.command.join(" ") : arg);
53
+ }
54
+ catch {
55
+ // free-form input
56
+ }
57
+ return arg ? `${name}: ${oneLine(arg, 80)}` : name;
58
+ }
59
+ /** The first real prompt in user messages whose lines were too long to parse (inline images). */
60
+ export function promptFromRaw(raw) {
61
+ for (const line of raw.split("\n")) {
62
+ if (!line.includes('"role":"user"'))
63
+ continue;
64
+ for (const t of textValues(line)) {
65
+ const v = t.trim();
66
+ if (v && !v.startsWith("<") && !v.startsWith("# AGENTS.md instructions"))
67
+ return v;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ export function readCodexLog(head, tail, rawHead) {
73
+ const meta = head.map(payload).find((p, i) => p && isObj(head[i]) && head[i].type === "session_meta");
74
+ const id = str(meta?.id) ?? str(meta?.session_id) ?? "unknown";
75
+ const cwd = str(meta?.cwd);
76
+ const exec = str(meta?.originator) === "codex_exec" || str(meta?.source) === "exec";
77
+ let prompt;
78
+ for (const r of head) {
79
+ const p = payload(r);
80
+ if (isObj(r) && r.type === "response_item" && p?.type === "message" && p.role === "user") {
81
+ // skip injected context: environment blocks, image tags, AGENTS.md instructions
82
+ const t = promptBlock(p);
83
+ if (t) {
84
+ prompt = t;
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ if (!prompt && rawHead)
90
+ prompt = promptFromRaw(rawHead());
91
+ const run = {
92
+ id,
93
+ runtime: "codex",
94
+ kind: exec ? "exec" : "session",
95
+ title: prompt ? oneLine(prompt, 60) : id.slice(0, 8),
96
+ cwd,
97
+ parent: parentFromCwd(cwd),
98
+ startedAt: str(meta?.timestamp) ?? (isObj(head[0]) ? str(head[0].timestamp) : undefined),
99
+ state: "working",
100
+ prompt: prompt ? oneLine(prompt, 400) : undefined,
101
+ };
102
+ let lastStart = -1;
103
+ let lastDone = -1;
104
+ let lastError = -1;
105
+ tail.forEach((r, i) => {
106
+ if (!isObj(r))
107
+ return;
108
+ const ts = str(r.timestamp);
109
+ if (ts && (!run.lastActivity || ts > run.lastActivity))
110
+ run.lastActivity = ts;
111
+ const p = payload(r);
112
+ if (!p)
113
+ return;
114
+ if (r.type === "event_msg" && p.type === "task_started")
115
+ lastStart = i;
116
+ if (r.type === "event_msg" && p.type === "task_complete") {
117
+ lastDone = i;
118
+ const msg = str(p.last_agent_message);
119
+ if (msg)
120
+ run.action = oneLine(msg, 100);
121
+ }
122
+ if (r.type === "event_msg" && (p.type === "error" || p.type === "turn_aborted"))
123
+ lastError = i;
124
+ if (r.type === "response_item" && (p.type === "custom_tool_call" || p.type === "function_call")) {
125
+ if (i > lastDone)
126
+ run.action = toolSummary(p);
127
+ }
128
+ if (r.type === "response_item" && p.type === "message" && p.role === "assistant" && i > lastDone) {
129
+ const t = messageText(p);
130
+ if (t)
131
+ run.action = oneLine(t, 100);
132
+ }
133
+ });
134
+ if (lastError > Math.max(lastStart, lastDone))
135
+ run.state = "failed";
136
+ else if (lastDone > lastStart)
137
+ run.state = exec ? "done" : "idle";
138
+ return run;
139
+ }
140
+ function dayDirs(root, sinceMs, now) {
141
+ const out = [];
142
+ const day = 86_400_000;
143
+ for (let t = sinceMs - day; t <= now + day; t += day) {
144
+ const d = new Date(t);
145
+ const y = String(d.getFullYear());
146
+ const m = String(d.getMonth() + 1).padStart(2, "0");
147
+ const dd = String(d.getDate()).padStart(2, "0");
148
+ const dir = path.join(root, y, m, dd);
149
+ if (!out.includes(dir))
150
+ out.push(dir);
151
+ }
152
+ return out;
153
+ }
154
+ export const codex = {
155
+ name: "codex",
156
+ scan(opts) {
157
+ const root = codexSessionsDir();
158
+ const runs = [];
159
+ for (const dir of dayDirs(root, opts.sinceMs, opts.now)) {
160
+ let files;
161
+ try {
162
+ files = fs.readdirSync(dir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl"));
163
+ }
164
+ catch {
165
+ continue;
166
+ }
167
+ for (const f of files) {
168
+ const file = path.join(dir, f);
169
+ try {
170
+ if (fs.statSync(file).mtimeMs < opts.sinceMs)
171
+ continue;
172
+ const run = { ...readCodexLog(readHeadLines(file, HEAD_BYTES), readTailLines(file, TAIL_BYTES), () => readHeadText(file, RAW_HEAD_BYTES)), source: file };
173
+ run.activity = logActivity(file, opts.now);
174
+ applyStall(run, undefined, opts.now);
175
+ runs.push(run);
176
+ }
177
+ catch (err) {
178
+ runs.push({ id: f, runtime: "codex", kind: "exec", title: f, state: "unknown", source: file, error: `could not read: ${err.message}` });
179
+ }
180
+ }
181
+ }
182
+ return runs;
183
+ },
184
+ };
185
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1,18 @@
1
+ import { claudeCode } from "./claudeCode.js";
2
+ import { codex } from "./codex.js";
3
+ export const adapters = [claudeCode, codex];
4
+ export function scanAll(opts, only) {
5
+ const runs = [];
6
+ for (const a of adapters) {
7
+ if (only && !only.includes(a.name))
8
+ continue;
9
+ try {
10
+ runs.push(...a.scan(opts));
11
+ }
12
+ catch (err) {
13
+ runs.push({ id: `${a.name}-error`, runtime: a.name, kind: "session", title: `${a.name} adapter failed`, state: "unknown", error: err.message });
14
+ }
15
+ }
16
+ return runs;
17
+ }
18
+ //# sourceMappingURL=index.js.map