handmux 0.22.0 → 0.23.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 (42) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/handmux.js +29 -28
  4. package/hooks/handmux-notify.sh +2 -4
  5. package/hooks/handmux-write.cjs +2 -24
  6. package/package.json +4 -4
  7. package/public/assets/index-C3N7OkyP.css +32 -0
  8. package/public/assets/index-D1DWcC5u.js +337 -0
  9. package/public/index.html +2 -2
  10. package/src/agents/claude.js +2 -0
  11. package/src/agents/codex.js +60 -13
  12. package/src/agents/index.js +2 -3
  13. package/src/browser/manager.js +37 -11
  14. package/src/browser/publicProxy.js +23 -1
  15. package/src/browser/routes.js +9 -0
  16. package/src/browser/siteVersion.js +124 -0
  17. package/src/claudeEvents.js +70 -27
  18. package/src/cli/codexManaged.js +77 -0
  19. package/src/cli/hookScaffold.js +8 -10
  20. package/src/cli/i18n/en.js +5 -8
  21. package/src/cli/i18n/zh.js +5 -8
  22. package/src/cli/legacyCodexHooks.js +49 -0
  23. package/src/codexAppServer.js +1620 -0
  24. package/src/codexDiff.js +108 -0
  25. package/src/codexTranscriptParse.js +413 -0
  26. package/{hooks/handmux-codex-usage.cjs → src/codexUsageSnapshot.cjs} +3 -15
  27. package/src/httpApi.js +4 -1
  28. package/src/orphans.js +3 -1
  29. package/src/routes/codex.js +473 -0
  30. package/src/routes/sessions.js +7 -2
  31. package/src/routes/system.js +9 -22
  32. package/src/routes/transcript.js +75 -19
  33. package/src/server.js +21 -13
  34. package/src/tmux/commands.js +11 -3
  35. package/src/transcriptReader.js +5 -5
  36. package/src/usage.js +4 -5
  37. package/src/workspace/agentRunner.js +4 -1
  38. package/src/workspace/capture.js +54 -2
  39. package/src/workspace/checkpointer.js +6 -2
  40. package/public/assets/index-CRAlQZDz.css +0 -32
  41. package/public/assets/index-CzlitgRX.js +0 -335
  42. package/src/cli/codexHooks.js +0 -123
@@ -0,0 +1,108 @@
1
+ const HUNK_HEADER = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
2
+
3
+ function changedLines(lines) {
4
+ return lines.filter((line) => /^[+-]/.test(line)
5
+ && !line.startsWith('+++') && !line.startsWith('---'));
6
+ }
7
+
8
+ export function parseUnifiedDiff(value, { created = false } = {}) {
9
+ const lines = String(value || '').split('\n');
10
+ let added = 0;
11
+ let removed = 0;
12
+ const hunks = [];
13
+ let current = null;
14
+ const flush = () => {
15
+ if (current?.lines.length) hunks.push(current);
16
+ current = null;
17
+ };
18
+
19
+ for (const line of lines) {
20
+ const header = HUNK_HEADER.exec(line);
21
+ if (header) {
22
+ flush();
23
+ current = { oldStart: Number(header[1]), newStart: Number(header[2]), lines: [] };
24
+ continue;
25
+ }
26
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
27
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++;
28
+ if (current && /^[ +\-]/.test(line) && !line.startsWith('+++') && !line.startsWith('---')) {
29
+ current.lines.push(line);
30
+ }
31
+ }
32
+ flush();
33
+ return {
34
+ added,
35
+ removed,
36
+ hunks: hunks.length ? hunks : null,
37
+ ...(created ? { created: true } : {}),
38
+ };
39
+ }
40
+
41
+ function applyPatchSection(value, path) {
42
+ const lines = String(value || '').split('\n');
43
+ let active = false;
44
+ const section = [];
45
+ for (const line of lines) {
46
+ const header = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/.exec(line);
47
+ if (header) {
48
+ if (active) break;
49
+ active = header[1].trim() === path;
50
+ continue;
51
+ }
52
+ if (active && line === '*** End Patch') break;
53
+ if (active) section.push(line);
54
+ }
55
+ return section;
56
+ }
57
+
58
+ function diffKey(lines) {
59
+ return changedLines(lines).join('\0');
60
+ }
61
+
62
+ function sameFilePath(left, right) {
63
+ const a = String(left || '').replaceAll('\\', '/').replace(/^\.\//, '');
64
+ const b = String(right || '').replaceAll('\\', '/').replace(/^\.\//, '');
65
+ return a === b || a.endsWith(`/${b}`) || b.endsWith(`/${a}`);
66
+ }
67
+
68
+ function candidateDiffs(thread) {
69
+ const candidates = [];
70
+ for (const turn of thread?.turns || []) {
71
+ for (const item of turn?.items || []) {
72
+ if (item?.type !== 'fileChange') continue;
73
+ for (const change of item.changes || []) {
74
+ const diff = parseUnifiedDiff(change?.diff, { created: change?.kind?.type === 'add' });
75
+ if (!diff.hunks) continue;
76
+ candidates.push({
77
+ turnId: turn.id || null,
78
+ path: change?.path || '',
79
+ key: diffKey(String(change?.diff || '').split('\n')),
80
+ diff,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return candidates;
86
+ }
87
+
88
+ export function enrichCodexFileDiffs(messages, thread) {
89
+ const candidates = candidateDiffs(thread);
90
+ const used = new Set();
91
+ return messages.map((message) => {
92
+ const tool = message?.tool;
93
+ if (tool?.name !== 'apply_patch' || !tool.diff || tool.diff.hunks?.length) return message;
94
+ const path = tool.input?.file_path || '';
95
+ const key = diffKey(applyPatchSection(tool.input?.patch, path));
96
+ const matches = (candidate, index) => !used.has(index)
97
+ && sameFilePath(candidate.path, path)
98
+ && candidate.diff.added === tool.diff.added
99
+ && candidate.diff.removed === tool.diff.removed
100
+ && (!message.turnId || !candidate.turnId || candidate.turnId === message.turnId);
101
+ let index = candidates.findIndex((candidate, candidateIndex) => matches(candidate, candidateIndex)
102
+ && (!key || !candidate.key || candidate.key === key));
103
+ if (index < 0 && !key) index = candidates.findIndex(matches);
104
+ if (index < 0) return message;
105
+ used.add(index);
106
+ return { ...message, tool: { ...tool, diff: candidates[index].diff } };
107
+ });
108
+ }
@@ -0,0 +1,413 @@
1
+ // Codex's rollout JSONL is the durable, ordered conversation log. App Server notifications drive live
2
+ // controls/status, but its thread snapshots currently omit some completed tools, so transcript rendering
3
+ // must come from this log alone. event_msg mirrors response_item content and is deliberately ignored.
4
+ const SLASH_CMD_RE = /^\/([a-z][\w-]*)(?:\s+([^\n]*))?$/i;
5
+ const OUTPUT_CAP = 100_000;
6
+ const SCRIPT_CAP = 4_000;
7
+
8
+ const cap = (value, limit) => {
9
+ const text = String(value ?? '');
10
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
11
+ };
12
+
13
+ // Codex persists its injected context as role=user response items. Match only its reserved envelope roots:
14
+ // user prompts may legitimately begin with ordinary HTML/XML and must remain part of the conversation.
15
+ export function isCodexSyntheticUserText(value) {
16
+ const text = String(value ?? '');
17
+ return /^\s*<(?:environment_context|permissions(?:\s+instructions)?|collaboration_mode|apps_instructions|plugins_instructions|skills_instructions|recommended_plugins|user_action|user_shell_command|image|turn_aborted)(?:\s|>|\/)/.test(text)
18
+ || /^\s*# AGENTS\.md instructions for [^\r\n]+\r?\n\s*<INSTRUCTIONS>(?:\r?\n|$)/.test(text);
19
+ }
20
+
21
+ function messageText(content) {
22
+ if (typeof content === 'string') return content;
23
+ if (!Array.isArray(content)) return '';
24
+ return content
25
+ .map((item) => (item && ['input_text', 'output_text', 'text'].includes(item.type)) ? (item.text || '') : '')
26
+ .join('');
27
+ }
28
+
29
+ function parseInput(value) {
30
+ if (value && typeof value === 'object') return value;
31
+ if (typeof value !== 'string' || !value.trim()) return {};
32
+ try {
33
+ const parsed = JSON.parse(value);
34
+ return parsed && typeof parsed === 'object' ? parsed : { value: parsed };
35
+ } catch { return { value }; }
36
+ }
37
+
38
+ function outputText(value) {
39
+ if (typeof value === 'string') {
40
+ const trimmed = value.trim();
41
+ if ((trimmed.startsWith('[') || trimmed.startsWith('{')) && trimmed.length < OUTPUT_CAP * 2) {
42
+ try { return outputText(JSON.parse(trimmed)); } catch { /* keep original */ }
43
+ }
44
+ return cap(value, OUTPUT_CAP);
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return cap(value.map((item) => {
48
+ if (item && typeof item === 'object' && typeof item.text === 'string') return item.text;
49
+ return outputText(item);
50
+ }).filter(Boolean).join('\n'), OUTPUT_CAP);
51
+ }
52
+ if (value && typeof value === 'object') {
53
+ if (Object.keys(value).length === 0) return '';
54
+ if (typeof value.output === 'string') return cap(value.output, OUTPUT_CAP);
55
+ try { return cap(JSON.stringify(value, null, 2), OUTPUT_CAP); } catch { return ''; }
56
+ }
57
+ return value == null ? '' : cap(value, OUTPUT_CAP);
58
+ }
59
+
60
+ function classicOutput(value) {
61
+ const raw = outputText(value);
62
+ const exit = raw.match(/Process exited with code\s+(-?\d+)/i);
63
+ const marker = raw.match(/(?:^|\n)Output:\n?/i);
64
+ const result = marker ? raw.slice(marker.index + marker[0].length) : raw;
65
+ const declined = /^aborted by user(?:\s+after\s+[\d.]+s)?\s*$/i.test(result.trim());
66
+ const isError = !!(exit && Number(exit[1]) !== 0);
67
+ return {
68
+ result,
69
+ isError,
70
+ outcome: declined ? 'declined' : isError ? 'failed' : 'success',
71
+ };
72
+ }
73
+
74
+ function resultOutcome(value, result) {
75
+ if (/^aborted by user(?:\s+after\s+[\d.]+s)?\s*$/i.test(String(result).trim())) return 'declined';
76
+ if (value && typeof value === 'object') {
77
+ if (value.success === false) return 'failed';
78
+ if (typeof value.exit_code === 'number') return value.exit_code === 0 ? 'success' : 'failed';
79
+ }
80
+ return 'success';
81
+ }
82
+
83
+ // Bounded extraction only: find known tools.* calls without evaluating generated JavaScript.
84
+ function callArgument(script, open) {
85
+ let depth = 1;
86
+ let quote = null;
87
+ let escaped = false;
88
+ for (let i = open + 1; i < script.length; i++) {
89
+ const ch = script[i];
90
+ if (quote) {
91
+ if (escaped) escaped = false;
92
+ else if (ch === '\\') escaped = true;
93
+ else if (ch === quote) quote = null;
94
+ continue;
95
+ }
96
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
97
+ if (ch === '(' || ch === '{' || ch === '[') depth++;
98
+ else if (ch === ')' || ch === '}' || ch === ']') {
99
+ depth--;
100
+ if (depth === 0) return { text: script.slice(open + 1, i).trim(), end: i + 1 };
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function decodeJsString(raw) {
107
+ if (!raw || !['"', "'", '`'].includes(raw[0]) || raw.at(-1) !== raw[0]) return null;
108
+ if (raw[0] === '"') {
109
+ try { return JSON.parse(raw); } catch { return null; }
110
+ }
111
+ const body = raw.slice(1, -1);
112
+ if (raw[0] === '`' && body.includes('${')) return null;
113
+ return body.replace(/\\([\\'`nrt])/g, (_match, char) => ({ n: '\n', r: '\r', t: '\t' }[char] ?? char));
114
+ }
115
+
116
+ function stringTokenAt(script, start) {
117
+ const quote = script[start];
118
+ if (!['"', "'", '`'].includes(quote)) return null;
119
+ let escaped = false;
120
+ for (let i = start + 1; i < script.length; i++) {
121
+ const ch = script[i];
122
+ if (escaped) escaped = false;
123
+ else if (ch === '\\') escaped = true;
124
+ else if (ch === quote) return script.slice(start, i + 1);
125
+ }
126
+ return null;
127
+ }
128
+
129
+ function resolveStringArgument(script, raw, before) {
130
+ const direct = decodeJsString(raw);
131
+ if (direct != null || !/^[A-Za-z_$][\w$]*$/.test(raw)) return direct;
132
+ const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
133
+ const declaration = new RegExp(`(?:const|let|var)\\s+${escaped}\\s*=\\s*`, 'g');
134
+ let match;
135
+ let token = null;
136
+ while ((match = declaration.exec(script)) && match.index < before) {
137
+ token = stringTokenAt(script, declaration.lastIndex);
138
+ }
139
+ return token ? decodeJsString(token) : null;
140
+ }
141
+
142
+ function splitTopLevel(value, separator) {
143
+ const parts = [];
144
+ let start = 0;
145
+ let depth = 0;
146
+ let quote = null;
147
+ let escaped = false;
148
+ for (let i = 0; i < value.length; i++) {
149
+ const ch = value[i];
150
+ if (quote) {
151
+ if (escaped) escaped = false;
152
+ else if (ch === '\\') escaped = true;
153
+ else if (ch === quote) quote = null;
154
+ continue;
155
+ }
156
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
157
+ if (ch === '(' || ch === '{' || ch === '[') depth++;
158
+ else if (ch === ')' || ch === '}' || ch === ']') depth--;
159
+ else if (ch === separator && depth === 0) {
160
+ parts.push(value.slice(start, i));
161
+ start = i + 1;
162
+ }
163
+ }
164
+ parts.push(value.slice(start));
165
+ return parts;
166
+ }
167
+
168
+ function parseJsLiteral(script, raw, before) {
169
+ const value = raw.trim();
170
+ const decoded = decodeJsString(value);
171
+ if (decoded != null) return decoded;
172
+ try { return JSON.parse(value); } catch { /* JavaScript object syntax is not strict JSON. */ }
173
+ const resolved = resolveStringArgument(script, value, before);
174
+ return resolved == null ? value : resolved;
175
+ }
176
+
177
+ // Orchestrated tool calls are stored as JavaScript source, and their arguments commonly use object-literal
178
+ // keys (`{ cmd: "pwd" }`) rather than strict JSON (`{"cmd":"pwd"}`). Parse only top-level literal fields;
179
+ // never evaluate generated code. Unknown expressions stay as source text so the detail view loses nothing.
180
+ function parseJsObjectInput(script, raw, before) {
181
+ const value = raw.trim();
182
+ if (!value.startsWith('{') || !value.endsWith('}')) return null;
183
+ const input = {};
184
+ for (const field of splitTopLevel(value.slice(1, -1), ',')) {
185
+ const colon = splitTopLevel(field, ':');
186
+ if (colon.length < 2) continue;
187
+ const rawKey = colon.shift().trim();
188
+ const key = decodeJsString(rawKey) ?? (/^[A-Za-z_$][\w$]*$/.test(rawKey) ? rawKey : null);
189
+ if (!key) continue;
190
+ input[key] = parseJsLiteral(script, colon.join(':').trim(), before);
191
+ }
192
+ return Object.keys(input).length ? input : null;
193
+ }
194
+
195
+ function parseToolInput(script, raw, before) {
196
+ const parsed = parseInput(raw);
197
+ if (parsed.value !== raw) return parsed;
198
+ return parseJsObjectInput(script, raw, before) || { script: raw };
199
+ }
200
+
201
+ function applyPatchCalls(patch) {
202
+ const header = /^\*\*\* (Add|Update|Delete) File: (.+)$/gm;
203
+ const sections = [];
204
+ let match;
205
+ while ((match = header.exec(patch))) {
206
+ sections.push({ kind: match[1], path: match[2].trim(), start: match.index });
207
+ }
208
+ if (!sections.length) return [{ name: 'apply_patch', input: { patch } }];
209
+ return sections.map((section, index) => {
210
+ const end = sections[index + 1]?.start ?? patch.indexOf('\n*** End Patch', section.start);
211
+ const body = patch.slice(section.start, end < 0 ? patch.length : end);
212
+ let added = 0;
213
+ let removed = 0;
214
+ for (const line of body.split('\n').slice(1)) {
215
+ if (line.startsWith('@@')) continue;
216
+ if (!/^[+\- ]/.test(line)) continue;
217
+ if (line.startsWith('+')) added++;
218
+ else if (line.startsWith('-')) removed++;
219
+ }
220
+ return {
221
+ name: 'apply_patch',
222
+ input: { file_path: section.path, patch },
223
+ diff: {
224
+ // apply_patch's persisted input normally uses bare `@@` markers with no file positions. Keep the
225
+ // stat, but never invent line zero; the transcript route enriches this from App Server's unified diff.
226
+ added, removed, hunks: null,
227
+ ...(section.kind === 'Add' ? { created: true } : {}),
228
+ },
229
+ };
230
+ });
231
+ }
232
+
233
+ function extractCustomCalls(script) {
234
+ const calls = [];
235
+ const re = /tools\.([A-Za-z][\w]*)\s*\(/g;
236
+ let match;
237
+ while ((match = re.exec(script))) {
238
+ const arg = callArgument(script, re.lastIndex - 1);
239
+ if (!arg) return null;
240
+ if (match[1] === 'apply_patch') {
241
+ const decoded = resolveStringArgument(script, arg.text, match.index);
242
+ calls.push(...(decoded == null
243
+ ? [{ name: 'apply_patch', input: { script: arg.text } }]
244
+ : applyPatchCalls(decoded)));
245
+ } else {
246
+ calls.push({ name: match[1], input: parseToolInput(script, arg.text, match.index) });
247
+ }
248
+ re.lastIndex = arg.end;
249
+ }
250
+ return calls.length ? calls : null;
251
+ }
252
+
253
+ function structuredResults(value) {
254
+ let parsed = value;
255
+ if (typeof parsed === 'string') {
256
+ try { parsed = JSON.parse(parsed); } catch { return null; }
257
+ }
258
+ if (Array.isArray(parsed)) {
259
+ for (let i = parsed.length - 1; i >= 0; i--) {
260
+ const text = parsed[i] && typeof parsed[i] === 'object' ? parsed[i].text : null;
261
+ if (typeof text !== 'string') continue;
262
+ try {
263
+ const object = JSON.parse(text);
264
+ if (object && typeof object === 'object' && !Array.isArray(object)) return Object.values(object);
265
+ } catch { /* try previous block */ }
266
+ }
267
+ return null;
268
+ }
269
+ return parsed && typeof parsed === 'object' ? Object.values(parsed) : null;
270
+ }
271
+
272
+ function applyCustomOutput(messages, value) {
273
+ const values = structuredResults(value);
274
+ if (values && values.length === messages.length) {
275
+ messages.forEach((message, index) => {
276
+ const result = values[index];
277
+ message.tool.result = outputText(result);
278
+ message.tool.outcome = resultOutcome(result, message.tool.result);
279
+ message.tool.isError = message.tool.outcome === 'failed';
280
+ });
281
+ return;
282
+ }
283
+ const raw = outputText(value).replace(/^Script completed[^\n]*\n(?:Wall time[^\n]*\n)?(?:Output:\n?)?/i, '');
284
+ const declined = /^aborted by user(?:\s+after\s+[\d.]+s)?\s*$/i.test(raw.trim());
285
+ messages.forEach((message, index) => {
286
+ message.tool.result = index === messages.length - 1 ? raw : '';
287
+ // A single extracted call has an exact persisted outcome. With several calls and one unstructured
288
+ // wrapper result, the rollout cannot identify which nested call produced it, so keep every card neutral.
289
+ message.tool.outcome = declined && messages.length === 1 ? 'declined'
290
+ : messages.length === 1 ? 'success' : 'completed';
291
+ });
292
+ }
293
+
294
+ export function createCodexTranscriptParser() {
295
+ const messages = [];
296
+ const pending = new Map();
297
+ let lineIndex = 0;
298
+
299
+ const toolMessage = (i, ts, name, input, diff, item) => ({
300
+ i, role: 'assistant', type: 'tool', ts,
301
+ ...(item?.internal_chat_message_metadata_passthrough?.turn_id
302
+ ? { turnId: item.internal_chat_message_metadata_passthrough.turn_id }
303
+ : {}),
304
+ tool: { name, input, result: null, isError: false, ...(diff ? { diff } : {}) },
305
+ });
306
+
307
+ function push(lines) {
308
+ for (const raw of lines) {
309
+ const i = lineIndex++;
310
+ const source = typeof raw === 'string' ? raw.trim() : '';
311
+ if (!source) continue;
312
+ let row;
313
+ try { row = JSON.parse(source); } catch { continue; }
314
+ const ts = typeof row.timestamp === 'string' ? row.timestamp : undefined;
315
+
316
+ if (row.type === 'compacted') { messages.push({ i, type: 'compact', ts }); continue; }
317
+ if (row.type !== 'response_item' || !row.payload || typeof row.payload !== 'object') continue;
318
+ const item = row.payload;
319
+
320
+ if (item.type === 'message') {
321
+ const text = messageText(item.content);
322
+ if (/^\s*<turn_aborted>/.test(text)) { messages.push({ i, type: 'interrupt', ts }); continue; }
323
+ if (item.role === 'developer') continue;
324
+ if (!['user', 'assistant'].includes(item.role) || !text.trim()) continue;
325
+ if (item.role === 'user') {
326
+ if (isCodexSyntheticUserText(text)) continue;
327
+ const slash = SLASH_CMD_RE.exec(text.trim());
328
+ if (slash) {
329
+ const marker = { i, type: 'slash', name: `/${slash[1]}`, ts };
330
+ if (slash[2]?.trim()) marker.args = slash[2].trim();
331
+ messages.push(marker);
332
+ continue;
333
+ }
334
+ }
335
+ const turnId = item.internal_chat_message_metadata_passthrough?.turn_id;
336
+ messages.push({ i, role: item.role, type: 'text', text, ts, ...(turnId ? { turnId } : {}) });
337
+ continue;
338
+ }
339
+
340
+ if (item.type === 'reasoning') {
341
+ const text = (Array.isArray(item.summary) ? item.summary : [])
342
+ .map((part) => part?.text || '').filter(Boolean).join('\n');
343
+ if (text.trim()) messages.push({ i, role: 'assistant', type: 'thinking', text, ts });
344
+ continue;
345
+ }
346
+
347
+ if (item.type === 'function_call') {
348
+ const message = toolMessage(i, ts, item.name || '', parseInput(item.arguments), null, item);
349
+ messages.push(message);
350
+ if (item.call_id) pending.set(item.call_id, [message]);
351
+ continue;
352
+ }
353
+ if (item.type === 'function_call_output') {
354
+ const targets = item.call_id && pending.get(item.call_id);
355
+ if (targets?.length === 1) {
356
+ const output = classicOutput(item.output);
357
+ targets[0].tool.result = output.result;
358
+ targets[0].tool.isError = output.isError;
359
+ targets[0].tool.outcome = output.outcome;
360
+ pending.delete(item.call_id);
361
+ }
362
+ continue;
363
+ }
364
+
365
+ if (item.type === 'tool_search_call') {
366
+ const message = toolMessage(i, ts, 'tool_search', parseInput(item.arguments), null, item);
367
+ if (item.status && item.status !== 'in_progress') {
368
+ message.tool.result = '';
369
+ message.tool.isError = item.status === 'failed';
370
+ message.tool.outcome = item.status === 'failed' ? 'failed' : 'success';
371
+ }
372
+ messages.push(message);
373
+ if (item.call_id) pending.set(item.call_id, [message]);
374
+ continue;
375
+ }
376
+ if (item.type === 'tool_search_output') {
377
+ const targets = item.call_id && pending.get(item.call_id);
378
+ if (targets?.length === 1) {
379
+ targets[0].tool.result = outputText(item.tools);
380
+ targets[0].tool.isError = item.status === 'failed';
381
+ targets[0].tool.outcome = item.status === 'failed' ? 'failed' : 'success';
382
+ pending.delete(item.call_id);
383
+ }
384
+ continue;
385
+ }
386
+
387
+ if (item.type === 'custom_tool_call') {
388
+ const script = typeof item.input === 'string' ? item.input : '';
389
+ const calls = extractCustomCalls(script);
390
+ const targets = calls
391
+ ? calls.map((call) => toolMessage(i, ts, call.name, call.input, call.diff, item))
392
+ : [toolMessage(i, ts, item.name || 'exec', { script: cap(script, SCRIPT_CAP) }, null, item)];
393
+ messages.push(...targets);
394
+ if (item.call_id) pending.set(item.call_id, targets);
395
+ continue;
396
+ }
397
+ if (item.type === 'custom_tool_call_output') {
398
+ const targets = item.call_id && pending.get(item.call_id);
399
+ if (targets) {
400
+ applyCustomOutput(targets, item.output);
401
+ pending.delete(item.call_id);
402
+ }
403
+ }
404
+ }
405
+ return messages;
406
+ }
407
+
408
+ return { push, messages };
409
+ }
410
+
411
+ export function parseCodexTranscript(lines) {
412
+ return createCodexTranscriptParser().push(lines);
413
+ }
@@ -1,6 +1,5 @@
1
- #!/usr/bin/env node
2
- // Persistent machine-wide Codex usage snapshot. This file is standalone CommonJS because hook installers
3
- // copy it into ~/.codex/hooks/, while the ESM server also requires the bundled copy directly.
1
+ // Persistent machine-wide Codex usage snapshot utilities. Kept as CommonJS so the ESM usage service can
2
+ // load the same bounded rollout reader without adding a second package boundary.
4
3
  const fs = require('node:fs');
5
4
  const path = require('node:path');
6
5
 
@@ -127,15 +126,4 @@ function writeSnapshot(snapshotPath, usage, { checkedAt } = {}) {
127
126
  }
128
127
  }
129
128
 
130
- function captureTranscript(transcriptPath, snapshotPath) {
131
- const usage = readLatestUsage(transcriptPath);
132
- if (!usage) return readSnapshot(snapshotPath)?.usage || null;
133
- return writeSnapshot(snapshotPath, usage);
134
- }
135
-
136
- module.exports = {
137
- readLatestUsage,
138
- readSnapshot,
139
- writeSnapshot,
140
- captureTranscript,
141
- };
129
+ module.exports = { readLatestUsage, readSnapshot, writeSnapshot };
package/src/httpApi.js CHANGED
@@ -25,6 +25,7 @@ import { transcriptRoutes } from './routes/transcript.js';
25
25
  import { DEFAULT_SHORTCUTS } from './shortcutConfig.js';
26
26
  import { workspaceRoutes } from './routes/workspace.js';
27
27
  import { browserRoutes } from './browser/routes.js';
28
+ import { codexRoutes } from './routes/codex.js';
28
29
 
29
30
  // Re-exported for tests (test/keys.test.js) and any caller that imported it by this path historically.
30
31
  export { isAllowedKey } from './routes/terminal.js';
@@ -38,6 +39,7 @@ export function createApiRouter({
38
39
  browserBootstrap,
39
40
  previewDomain,
40
41
  workspace,
42
+ codexApp,
41
43
  home = homedir(), stateFile = process.env.CLAUDE_STATE_FILE || claudeStatePath(homedir()),
42
44
  } = {}) {
43
45
  const r = express.Router();
@@ -47,7 +49,7 @@ export function createApiRouter({
47
49
 
48
50
  const deps = {
49
51
  token, commands, docs, git, push, notifications, claudeEvents,
50
- uploadExts, maxUploadBytes, asrEnv, previews, shortcuts, home, stateFile, workspace, browser, browserBootstrap, previewDomain,
52
+ uploadExts, maxUploadBytes, asrEnv, previews, shortcuts, home, stateFile, workspace, browser, browserBootstrap, previewDomain, codexApp,
51
53
  };
52
54
 
53
55
  r.use(sessionRoutes(deps));
@@ -63,6 +65,7 @@ export function createApiRouter({
63
65
  // the worker-owned bootstrap ticket store.
64
66
  r.use('/browser-proxy', browserRoutes(browser ? deps : { ...deps, previewDomain: null }));
65
67
  r.use(transcriptRoutes(deps));
68
+ r.use(codexRoutes(deps));
66
69
  if (workspace) r.use(workspaceRoutes(deps));
67
70
 
68
71
  return r;
package/src/orphans.js CHANGED
@@ -65,7 +65,9 @@ export async function takeoverOrphan(
65
65
  if (o.sessionId !== sessionId) return { error: 'session changed', status: 409 };
66
66
  if (!o.cwd) return { error: 'no cwd', status: 409 };
67
67
  const agent = getAgent(o.agent);
68
- const cmd = agent.sessions.resumeArgs(sessionId).join(' ');
68
+ const cmd = agent.sessions.managedResumeCmd
69
+ ? agent.sessions.managedResumeCmd(sessionId)
70
+ : agent.sessions.resumeArgs(sessionId).join(' ');
69
71
 
70
72
  let sid;
71
73
  let wid;