@hwip/cli 0.0.1 → 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.
Files changed (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,24 @@
1
+ /** The table hwip writes into a config that has none. No `env` block, unlike
2
+ * the Cursor/Windsurf JSON entry: codex is a terminal app, so the server
3
+ * inherits the shell's `HWIP_API_KEY`, and an unexpanded `"${HWIP_API_KEY}"`
4
+ * literal would shadow the ~/.hwip/config.json fallback with junk (the same
5
+ * reasoning as opencode's). */
6
+ export declare const CODEX_MCP_TABLE: string;
7
+ /**
8
+ * The config with hwip's entry present and current, leaving every other byte
9
+ * alone. Appended at the end when absent: a table header ends the table before
10
+ * it, so nothing above can capture the keys.
11
+ */
12
+ export declare const upsertHwipMcpTable: (config: string) => string;
13
+ /** The config with hwip's entry and its sub-tables gone, or null when it was
14
+ * never there (so the caller can report "nothing to remove" rather than
15
+ * rewriting an untouched file). */
16
+ export declare const removeHwipMcpTable: (config: string) => string | null;
17
+ /**
18
+ * The launch argv codex has recorded for hwip, verbatim, or null when there is
19
+ * no entry. Same contract as `registeredMcpArgv` in verify.ts: returned
20
+ * unnormalized, because a stale entry is only visible if the reader does not
21
+ * reshape it into what it expects.
22
+ */
23
+ export declare const readHwipMcpArgv: (config: string) => string[] | null;
24
+ //# sourceMappingURL=codex-config.d.ts.map
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.upsertHwipMcpTable = exports.CODEX_MCP_TABLE = void 0;
4
+ /**
5
+ * The hwip entry inside `~/.codex/config.toml`, which Codex CLI owns.
6
+ *
7
+ * Codex is the one supported agent whose MCP registry is TOML, and the file is
8
+ * the user's: it carries their model, their approval settings, their other
9
+ * servers. So this module works at the text level on the one table hwip owns
10
+ * (`[mcp_servers.hwip]`) and never rewrites the document — a parse-and-emit
11
+ * round trip through a TOML library would reorder their keys and drop their
12
+ * comments, which is a worse failure than the entry being missing.
13
+ *
14
+ * What that promise costs, stated once here because every function below is
15
+ * shaped by it:
16
+ *
17
+ * - **Sub-tables are part of the entry.** Codex writes per-tool settings as
18
+ * `[mcp_servers.hwip.tools.hwip_ask]` when the user picks "always allow".
19
+ * Removal takes them along (a child of a deleted server is dead config);
20
+ * re-install keeps them, since they are the user's answer, not ours.
21
+ * - **Only `command` and `args` are ours.** Any other key in the table —
22
+ * `cwd`, `startup_timeout_sec`, a comment — survives a re-install.
23
+ * - **Comments before the next table belong to that table**, not to ours, so
24
+ * the region we replace or delete stops short of them.
25
+ *
26
+ * Install, uninstall and verify all go through here so this is stated once.
27
+ */
28
+ const mcp_command_js_1 = require("./mcp-command.js");
29
+ const [MCP_BIN, ...MCP_SUBCOMMAND] = mcp_command_js_1.MCP_LAUNCH_ARGV;
30
+ /** The table header, with or without the quotes TOML also allows. */
31
+ const HWIP_TABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\]$/;
32
+ /** A sub-table of ours: `[mcp_servers.hwip.tools.hwip_ask]` and friends. */
33
+ const HWIP_SUBTABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\..+\]$/;
34
+ /**
35
+ * The dotted/inline form — `mcp_servers.hwip = { command = "npx", … }` — which
36
+ * is valid TOML and which older hand-written configs use. It has to be found,
37
+ * because appending a header table beside it is a duplicate-key error that
38
+ * makes codex reject the WHOLE config, hwip's entry and the user's alike.
39
+ */
40
+ const HWIP_INLINE_ENTRY = /^mcp_servers\.(?:hwip|"hwip")\s*=/;
41
+ const ANY_TABLE_HEADER = /^\[/;
42
+ const isHwipDefinition = (line) => HWIP_TABLE_HEADER.test(line.trim()) || HWIP_INLINE_ENTRY.test(line.trim());
43
+ /** The keys hwip owns; everything else in the table is the user's. */
44
+ const OWNED_KEY = /^(?:command|args)\s*=/;
45
+ const commandLine = `command = "${MCP_BIN}"`;
46
+ const argsLine = `args = [${MCP_SUBCOMMAND.map((a) => `"${a}"`).join(', ')}]`;
47
+ /** The table hwip writes into a config that has none. No `env` block, unlike
48
+ * the Cursor/Windsurf JSON entry: codex is a terminal app, so the server
49
+ * inherits the shell's `HWIP_API_KEY`, and an unexpanded `"${HWIP_API_KEY}"`
50
+ * literal would shadow the ~/.hwip/config.json fallback with junk (the same
51
+ * reasoning as opencode's). */
52
+ exports.CODEX_MCP_TABLE = ['[mcp_servers.hwip]', commandLine, argsLine].join('\n');
53
+ /** CRLF in, CRLF out — a Windows config must not come back with mixed endings. */
54
+ const eolOf = (config) => (config.includes('\r\n') ? '\r\n' : '\n');
55
+ const splitLines = (config) => config.split(/\r?\n/);
56
+ /** Line index of hwip's definition — header or inline — or -1. */
57
+ const definitionIndex = (lines) => lines.findIndex((line) => isHwipDefinition(line));
58
+ /** One past the last line of hwip's own key/value body (the next table header
59
+ * of any kind, or EOF). Meaningless for the inline form, which is one line. */
60
+ const bodyEnd = (lines, start) => {
61
+ const next = lines.slice(start + 1).findIndex((line) => ANY_TABLE_HEADER.test(line.trim()));
62
+ return next === -1 ? lines.length : start + 1 + next;
63
+ };
64
+ /** One past the last line of the whole entry: body plus every sub-table of
65
+ * ours, stopping at the first header that belongs to someone else. */
66
+ const entryEnd = (lines, start) => {
67
+ let end = bodyEnd(lines, start);
68
+ while (end < lines.length && HWIP_SUBTABLE_HEADER.test(lines[end].trim())) {
69
+ end = bodyEnd(lines, end);
70
+ }
71
+ return end;
72
+ };
73
+ /**
74
+ * `end` walked back past the blank lines and comments that precede whatever
75
+ * comes next. A comment sitting above `[mcp_servers.graft]` documents THAT
76
+ * server; deleting it with our table is the exact loss this module exists to
77
+ * avoid.
78
+ */
79
+ const withoutTrailingPreamble = (lines, start, end) => {
80
+ let cut = end;
81
+ while (cut > start + 1) {
82
+ const line = lines[cut - 1].trim();
83
+ if (line === '' || line.startsWith('#'))
84
+ cut -= 1;
85
+ else
86
+ break;
87
+ }
88
+ return cut;
89
+ };
90
+ /** The document, ending in exactly one newline, so writing a config that
91
+ * already carries the table is a no-op rather than a one-byte diff. */
92
+ const rejoin = (lines, eol) => `${lines.join(eol).replace(/(?:\r?\n)+$/, '')}${eol}`;
93
+ /**
94
+ * The config with hwip's entry present and current, leaving every other byte
95
+ * alone. Appended at the end when absent: a table header ends the table before
96
+ * it, so nothing above can capture the keys.
97
+ */
98
+ const upsertHwipMcpTable = (config) => {
99
+ const eol = eolOf(config);
100
+ const lines = splitLines(config);
101
+ const start = definitionIndex(lines);
102
+ if (start === -1) {
103
+ const body = config.replace(/(?:\r?\n)+$/, '');
104
+ const table = exports.CODEX_MCP_TABLE.split('\n').join(eol);
105
+ return `${body ? `${body}${eol}${eol}` : ''}${table}${eol}`;
106
+ }
107
+ // The inline form is a single line and cannot hold keys of ours to keep.
108
+ if (HWIP_INLINE_ENTRY.test(lines[start].trim())) {
109
+ const table = exports.CODEX_MCP_TABLE.split('\n');
110
+ return rejoin([...lines.slice(0, start), ...table, ...lines.slice(start + 1)], eol);
111
+ }
112
+ const end = withoutTrailingPreamble(lines, start, bodyEnd(lines, start));
113
+ const kept = lines.slice(start + 1, end).filter((line) => !OWNED_KEY.test(line.trim()));
114
+ return rejoin([
115
+ ...lines.slice(0, start),
116
+ '[mcp_servers.hwip]',
117
+ commandLine,
118
+ argsLine,
119
+ ...kept,
120
+ ...lines.slice(end),
121
+ ], eol);
122
+ };
123
+ exports.upsertHwipMcpTable = upsertHwipMcpTable;
124
+ /** The config with hwip's entry and its sub-tables gone, or null when it was
125
+ * never there (so the caller can report "nothing to remove" rather than
126
+ * rewriting an untouched file). */
127
+ const removeHwipMcpTable = (config) => {
128
+ const eol = eolOf(config);
129
+ const lines = splitLines(config);
130
+ const start = definitionIndex(lines);
131
+ if (start === -1)
132
+ return null;
133
+ const end = HWIP_INLINE_ENTRY.test(lines[start].trim())
134
+ ? start + 1
135
+ : withoutTrailingPreamble(lines, start, entryEnd(lines, start));
136
+ const kept = [...lines.slice(0, start), ...lines.slice(end)];
137
+ // The removed entry left its blank separator behind; collapse the run so
138
+ // repeated install/uninstall cycles cannot grow the file.
139
+ return `${kept.join(eol).replace(/(?:\r?\n){3,}/g, `${eol}${eol}`).replace(/(?:\r?\n)+$/, '')}${eol}`;
140
+ };
141
+ exports.removeHwipMcpTable = removeHwipMcpTable;
142
+ /**
143
+ * The launch argv codex has recorded for hwip, verbatim, or null when there is
144
+ * no entry. Same contract as `registeredMcpArgv` in verify.ts: returned
145
+ * unnormalized, because a stale entry is only visible if the reader does not
146
+ * reshape it into what it expects.
147
+ */
148
+ const readHwipMcpArgv = (config) => {
149
+ const lines = splitLines(config);
150
+ const start = definitionIndex(lines);
151
+ if (start === -1)
152
+ return null;
153
+ // Joined, not scanned line by line: `args` is often written across several
154
+ // lines, and a line-wise reader drops it — which verify would then report
155
+ // as a stale registration for an entry that is in fact current.
156
+ const body = HWIP_INLINE_ENTRY.test(lines[start].trim())
157
+ ? lines[start]
158
+ : lines.slice(start + 1, bodyEnd(lines, start)).join('\n');
159
+ const command = body.match(/(?:^|[\s,{])command\s*=\s*["']([^"']+)["']/)?.[1];
160
+ if (!command)
161
+ return null;
162
+ const rawArgs = body.match(/(?:^|[\s,{])args\s*=\s*\[([^\]]*)\]/)?.[1] ?? '';
163
+ const args = rawArgs
164
+ .split(',')
165
+ .map((a) => a.trim().replace(/^["']|["']$/g, ''))
166
+ .filter(Boolean);
167
+ return [command, ...args];
168
+ };
169
+ exports.readHwipMcpArgv = readHwipMcpArgv;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Codex's rollout format, projected into the events the timeline draws.
3
+ *
4
+ * The third projector. Like pi's, everything here is Codex's vocabulary and
5
+ * belongs to Codex alone — `turn-watch` reaches it through `TranscriptProjector`
6
+ * and knows nothing about any of it.
7
+ *
8
+ * Shape measured 2026-09-14 against `~/.codex/sessions/2026/09/14/rollout-…jsonl`
9
+ * (77 lines, codex-cli 0.154.0, `gpt-5.6-luna`), cross-checked against the
10
+ * strings in the writer itself (`~/.codex/packages/standalone/current/bin/codex`)
11
+ * for the entry types that session happens not to contain.
12
+ *
13
+ * Every line is `{ timestamp, ordinal, type, payload }`. Two lanes describe the
14
+ * same turn, and this projector reads exactly one of them:
15
+ *
16
+ * - `response_item` — what goes to the model: `message`, `reasoning`,
17
+ * `custom_tool_call`, `function_call` and their `_output` twins.
18
+ * - `event_msg` — what the UI is told: `item_completed` wrapping a typed
19
+ * `item`, plus `token_count` and the task markers.
20
+ *
21
+ * `event_msg` wins, for three reasons measured in that session:
22
+ *
23
+ * 1. Its `UserMessage` items are the person's messages *only*. The
24
+ * `response_item` lane files Codex's own injections under `role: 'user'`
25
+ * too — the first user-role entry there is `# AGENTS.md instructions …`
26
+ * followed by `<environment_context>` — and stamps no provenance on them.
27
+ * Four user-role `response_item`s, three `UserMessage` items, and the one
28
+ * left out is the injection. Codex has already made this call; pi's
29
+ * projector had to guess at it with a wrapper regex because pi has no
30
+ * equivalent lane.
31
+ * 2. `CommandExecution` carries `parsed_cmd`, Codex's own reading of what a
32
+ * shell line does (`{ type: 'read', name: 'SKILL.md', path: '…' }`), plus
33
+ * `exit_code`. The `response_item` twin carries a JavaScript program —
34
+ * `const r = await tools.exec_command({cmd:"…"})` — whose command string
35
+ * would have to be parsed back out of source text to label a row.
36
+ * 3. One lane cannot double-count. Reading both would put every tool call on
37
+ * the timeline twice, and the two lanes share no id to merge on.
38
+ *
39
+ * The cost is latency, and it is real: an `item_completed` is written when the
40
+ * work finishes, so a two-minute build shows up two minutes in rather than at
41
+ * its start. The `response_item` lane is written at call time but cannot say
42
+ * whether the call succeeded — the honest row late beats the ambiguous row
43
+ * early, and a timeline that showed a call and never its outcome is the worse
44
+ * of the two failures.
45
+ *
46
+ * An item type this file does not name draws no row at all. Codex ships types no
47
+ * session on this machine has produced and will ship more; each needs a sample
48
+ * read before it can be projected, and until then a thinner timeline is the
49
+ * honest one. `FileChange` and `McpToolCall` were read from samples on
50
+ * 2026-10-02 (1 and 7 items) and are projected; `update` / `delete` changes and
51
+ * the failure forms rest on the writer's strings only, and say so where used.
52
+ */
53
+ import { type TranscriptProjector } from './transcript-tail.js';
54
+ /**
55
+ * Codex writes one line per completed item, so no event needs merging across
56
+ * lines — the same place this projector is simpler than Claude's.
57
+ */
58
+ export declare const projectCodexEntries: TranscriptProjector;
59
+ //# sourceMappingURL=codex-transcript.d.ts.map
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ /**
3
+ * Codex's rollout format, projected into the events the timeline draws.
4
+ *
5
+ * The third projector. Like pi's, everything here is Codex's vocabulary and
6
+ * belongs to Codex alone — `turn-watch` reaches it through `TranscriptProjector`
7
+ * and knows nothing about any of it.
8
+ *
9
+ * Shape measured 2026-09-14 against `~/.codex/sessions/2026/09/14/rollout-…jsonl`
10
+ * (77 lines, codex-cli 0.154.0, `gpt-5.6-luna`), cross-checked against the
11
+ * strings in the writer itself (`~/.codex/packages/standalone/current/bin/codex`)
12
+ * for the entry types that session happens not to contain.
13
+ *
14
+ * Every line is `{ timestamp, ordinal, type, payload }`. Two lanes describe the
15
+ * same turn, and this projector reads exactly one of them:
16
+ *
17
+ * - `response_item` — what goes to the model: `message`, `reasoning`,
18
+ * `custom_tool_call`, `function_call` and their `_output` twins.
19
+ * - `event_msg` — what the UI is told: `item_completed` wrapping a typed
20
+ * `item`, plus `token_count` and the task markers.
21
+ *
22
+ * `event_msg` wins, for three reasons measured in that session:
23
+ *
24
+ * 1. Its `UserMessage` items are the person's messages *only*. The
25
+ * `response_item` lane files Codex's own injections under `role: 'user'`
26
+ * too — the first user-role entry there is `# AGENTS.md instructions …`
27
+ * followed by `<environment_context>` — and stamps no provenance on them.
28
+ * Four user-role `response_item`s, three `UserMessage` items, and the one
29
+ * left out is the injection. Codex has already made this call; pi's
30
+ * projector had to guess at it with a wrapper regex because pi has no
31
+ * equivalent lane.
32
+ * 2. `CommandExecution` carries `parsed_cmd`, Codex's own reading of what a
33
+ * shell line does (`{ type: 'read', name: 'SKILL.md', path: '…' }`), plus
34
+ * `exit_code`. The `response_item` twin carries a JavaScript program —
35
+ * `const r = await tools.exec_command({cmd:"…"})` — whose command string
36
+ * would have to be parsed back out of source text to label a row.
37
+ * 3. One lane cannot double-count. Reading both would put every tool call on
38
+ * the timeline twice, and the two lanes share no id to merge on.
39
+ *
40
+ * The cost is latency, and it is real: an `item_completed` is written when the
41
+ * work finishes, so a two-minute build shows up two minutes in rather than at
42
+ * its start. The `response_item` lane is written at call time but cannot say
43
+ * whether the call succeeded — the honest row late beats the ambiguous row
44
+ * early, and a timeline that showed a call and never its outcome is the worse
45
+ * of the two failures.
46
+ *
47
+ * An item type this file does not name draws no row at all. Codex ships types no
48
+ * session on this machine has produced and will ship more; each needs a sample
49
+ * read before it can be projected, and until then a thinner timeline is the
50
+ * honest one. `FileChange` and `McpToolCall` were read from samples on
51
+ * 2026-10-02 (1 and 7 items) and are projected; `update` / `delete` changes and
52
+ * the failure forms rest on the writer's strings only, and say so where used.
53
+ */
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.projectCodexEntries = void 0;
56
+ const transcript_tail_js_1 = require("./transcript-tail.js");
57
+ /** The item types this projector knows. Anything else draws no row — see the file header. */
58
+ const CODEX_USER_MESSAGE = 'UserMessage';
59
+ const CODEX_AGENT_MESSAGE = 'AgentMessage';
60
+ const CODEX_REASONING = 'Reasoning';
61
+ const CODEX_COMMAND = 'CommandExecution';
62
+ const CODEX_EXTENSION = 'Extension';
63
+ const CODEX_FILE_CHANGE = 'FileChange';
64
+ const CODEX_MCP_CALL = 'McpToolCall';
65
+ /**
66
+ * Which `parsed_cmd` field reads as "what this call is about", first match wins.
67
+ *
68
+ * Codex's own names, and a short list because `parsed_cmd` is already the
69
+ * vendor's summary rather than raw argv: a `read` entry carries `name` (the
70
+ * basename) and `path`, a search carries `query`. `cmd` — the reconstructed
71
+ * command line, and the field most likely to hold a token or a signed URL — is
72
+ * absent for the same reason `command` is absent from Claude's list.
73
+ */
74
+ const CODEX_PARSED_KEYS = ['name', 'query', 'path'];
75
+ /**
76
+ * Codex nests JSON inside JSON: an item's `content` arrives as a string holding
77
+ * an encoded array. Null on anything that is not JSON, which the callers read as
78
+ * "nothing to take from here".
79
+ */
80
+ const safeParse = (raw) => {
81
+ try {
82
+ return JSON.parse(raw);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ };
88
+ /** Prose out of a Codex content array. */
89
+ const textOf = (content) => {
90
+ const items = typeof content === 'string' ? safeParse(content) : content;
91
+ if (!Array.isArray(items))
92
+ return '';
93
+ let text = '';
94
+ for (const block of items) {
95
+ if (!block || typeof block !== 'object')
96
+ continue;
97
+ const item = block;
98
+ // `UserMessage` blocks are typed `text`, `AgentMessage` blocks `Text` —
99
+ // measured, not assumed. The type IS checked, because the writer's
100
+ // content kinds include `SkillMention`, `Image` and `Audio`, and one of
101
+ // those carrying an incidental `text` field would otherwise be glued
102
+ // into the middle of what reads as the person's own sentence.
103
+ if (typeof item.type !== 'string' || item.type.toLowerCase() !== 'text')
104
+ continue;
105
+ if (typeof item.text === 'string')
106
+ text += item.text;
107
+ }
108
+ return text;
109
+ };
110
+ /**
111
+ * Whether a completed command actually succeeded.
112
+ *
113
+ * `exit_code` decides when there is one, and the absence of one is NOT success:
114
+ * an interrupted or aborted command completes as an item without ever having
115
+ * exited, and coercing that to 0 draws a green row for work that never
116
+ * finished. `status` is what Codex itself reports then — measured as
117
+ * `'completed'` on every item in the sampled session, alongside `failed` and
118
+ * `aborted` in the writer's own strings.
119
+ */
120
+ const succeeded = (item) => typeof item.exit_code === 'number' ? item.exit_code === 0 : item.status === 'completed';
121
+ const record = (value) => value && typeof value === 'object' ? value : null;
122
+ /**
123
+ * `+` / `-` lines of a unified diff, file header (`+++` / `---` before the first
124
+ * `@@`) excluded — after it, a removed `-- sql comment` is a line like any other.
125
+ * `unified_diff` is the field name in the writer's own strings; no `update`
126
+ * entry exists in any rollout measured here, so this reads the standard format
127
+ * and nothing Codex-specific.
128
+ */
129
+ const diffLineCounts = (diff) => {
130
+ let add = 0;
131
+ let del = 0;
132
+ let inHunk = false;
133
+ for (const line of diff.split('\n')) {
134
+ if (line.startsWith('@@'))
135
+ inHunk = true;
136
+ else if (!inHunk && (line.startsWith('+++') || line.startsWith('---')))
137
+ continue;
138
+ else if (line.startsWith('+'))
139
+ add++;
140
+ else if (line.startsWith('-'))
141
+ del++;
142
+ }
143
+ return { add, del };
144
+ };
145
+ /**
146
+ * What a `FileChange` row says about its patch: the first path, how many more
147
+ * there were, and numbers about the lines — never the lines. An `add` is
148
+ * measured (its `content` is the new file); an `update` reads its diff; a
149
+ * `delete` carries no count because no sample shows what it holds.
150
+ */
151
+ const fileChangeSummary = (changes) => {
152
+ // A list is not the path-keyed object measured in the sample; its index
153
+ // keys would draw as file names.
154
+ const entries = Array.isArray(changes) ? [] : Object.entries(record(changes) ?? {});
155
+ if (entries.length === 0)
156
+ return {};
157
+ let add = 0;
158
+ let del = 0;
159
+ for (const [, change] of entries) {
160
+ const c = record(change);
161
+ if (!c)
162
+ continue;
163
+ if (c.type === 'add' && typeof c.content === 'string')
164
+ add += (0, transcript_tail_js_1.resultLinesOf)(c.content);
165
+ else if (c.type === 'update' && typeof c.unified_diff === 'string') {
166
+ const counts = diffLineCounts(c.unified_diff);
167
+ add += counts.add;
168
+ del += counts.del;
169
+ }
170
+ }
171
+ const rest = entries.length - 1;
172
+ const target = (0, transcript_tail_js_1.oneLine)(entries[0][0].trim());
173
+ // The suffix is added after the clamp so a long path cannot cut it off.
174
+ const suffix = rest > 0 ? ` (+${rest} more)` : '';
175
+ return {
176
+ ...(target ? { target: (0, transcript_tail_js_1.clamp)(target, transcript_tail_js_1.MAX_EVENT_FIELD_CHARS - suffix.length) + suffix } : {}),
177
+ ...(add ? { add } : {}),
178
+ ...(del ? { del } : {}),
179
+ };
180
+ };
181
+ /**
182
+ * The usage numbers on a `token_count` event.
183
+ *
184
+ * `last_token_usage` is this turn's, `total_token_usage` the session's running
185
+ * sum — the running one would make every row report the whole session.
186
+ *
187
+ * One event per usage tick, NOT one per assistant message: the sampled session
188
+ * holds 7 `token_count` events against 5 `AgentMessage` items across 3 turns.
189
+ * `mid` therefore means "one usage tick" here where it means "one API message"
190
+ * for Claude. The viewer only dedupes on it, so the finer grain costs nothing —
191
+ * but it is not a message id and nothing downstream may read it as one.
192
+ *
193
+ * Codex reports `input_tokens` as the full prompt with `cached_input_tokens`
194
+ * already inside it (measured: 16054 input of which 11904 cached, totalling 16067 with
195
+ * 13 output), so unlike Claude's three separate counters it is already the
196
+ * context number and must not be added to.
197
+ */
198
+ const usageEvent = (payload, ordinal, index, model, at) => {
199
+ const info = record(payload.info);
200
+ const last = info && record(info.last_token_usage);
201
+ if (!last)
202
+ return null;
203
+ // Codex stamps no id on a usage event, so the line's own ordinal is the id:
204
+ // unique within a rollout and monotonic, which is all `mid` is for. A Codex
205
+ // release that renamed the field would otherwise drop every usage row,
206
+ // blanking the phone's token readout with no other symptom — so the line's
207
+ // timestamp and position stand in. The timestamp carries the weight: an
208
+ // index alone repeats across ticks (every tick has a `lines[0]`), and the
209
+ // viewer dedupes by `mid`, so two ticks' numbers would collapse into one.
210
+ const mid = typeof ordinal === 'number' ? String(ordinal) : `line-${at ?? 'unstamped'}-${index}`;
211
+ const reasoning = (0, transcript_tail_js_1.finiteNumber)(last.reasoning_output_tokens);
212
+ return {
213
+ kind: 'msg',
214
+ mid,
215
+ ...(model ? { model: (0, transcript_tail_js_1.clamp)(model) } : {}),
216
+ out: (0, transcript_tail_js_1.finiteNumber)(last.output_tokens),
217
+ ctx: (0, transcript_tail_js_1.finiteNumber)(last.input_tokens),
218
+ // Codex reports reasoning as tokens, not blocks. A non-zero count is
219
+ // still "it thought here", which is the only thing this field draws.
220
+ ...(reasoning ? { thinking: reasoning } : {}),
221
+ ...(at ? { at } : {}),
222
+ };
223
+ };
224
+ /**
225
+ * Codex writes one line per completed item, so no event needs merging across
226
+ * lines — the same place this projector is simpler than Claude's.
227
+ */
228
+ const projectCodexEntries = (lines, opts = {}) => {
229
+ const events = [];
230
+ // The model is stamped on `turn_context`, once per turn, ahead of the usage
231
+ // events it applies to. Carried forward rather than looked up, because a
232
+ // tail that begins mid-session may never see one.
233
+ let model;
234
+ for (let index = 0; index < lines.length; index++) {
235
+ const entry = record(safeParse(lines[index]));
236
+ if (!entry)
237
+ continue;
238
+ const at = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
239
+ const stamp = at ? { at } : {};
240
+ if (entry.type === 'turn_context') {
241
+ const payload = record(entry.payload);
242
+ if (payload && typeof payload.model === 'string')
243
+ model = payload.model;
244
+ continue;
245
+ }
246
+ // `session_meta` is the cwd header the resolver reads, `world_state` and
247
+ // `token_usage_record` are bookkeeping, and `response_item` is the lane
248
+ // this projector deliberately does not read.
249
+ if (entry.type !== 'event_msg')
250
+ continue;
251
+ const payload = record(entry.payload);
252
+ if (!payload)
253
+ continue;
254
+ if (payload.type === 'token_count') {
255
+ const usage = usageEvent(payload, entry.ordinal, index, model, at);
256
+ if (usage)
257
+ events.push(usage);
258
+ continue;
259
+ }
260
+ if (payload.type !== 'item_completed')
261
+ continue;
262
+ const item = record(payload.item);
263
+ if (!item || typeof item.type !== 'string' || typeof item.id !== 'string') {
264
+ opts.onUnknown?.('codex item without a type or id');
265
+ continue;
266
+ }
267
+ if (item.type === CODEX_USER_MESSAGE) {
268
+ const text = textOf(item.content).trim();
269
+ if (text)
270
+ events.push({ kind: 'prompt', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
271
+ continue;
272
+ }
273
+ if (item.type === CODEX_AGENT_MESSAGE) {
274
+ const text = textOf(item.content).trim();
275
+ if (text)
276
+ events.push({ kind: 'text', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
277
+ continue;
278
+ }
279
+ // Reasoning is what the agent considered, not what it did — dropped
280
+ // here exactly as thinking blocks are for Claude and pi. Its size still
281
+ // reaches the timeline, as the `thinking` field on the usage event.
282
+ if (item.type === CODEX_REASONING)
283
+ continue;
284
+ if (item.type === CODEX_COMMAND) {
285
+ const parsed = Array.isArray(item.parsed_cmd) ? record(item.parsed_cmd[0]) : null;
286
+ // Codex's own reading of the command — `read`, `search`, `unknown`.
287
+ // The raw `command` argv is never consulted: it is `['/bin/zsh',
288
+ // '-lc', <the whole script>]`, which is the shape that carries
289
+ // secrets.
290
+ const name = parsed && typeof parsed.type === 'string' ? parsed.type : 'exec';
291
+ const target = (0, transcript_tail_js_1.oneLine)((0, transcript_tail_js_1.targetFrom)(parsed, CODEX_PARSED_KEYS));
292
+ const resultLines = (0, transcript_tail_js_1.resultLinesOf)(item.aggregated_output);
293
+ events.push({ kind: 'tool', id: item.id, name: (0, transcript_tail_js_1.clamp)(name), ...(target ? { target } : {}), ...stamp });
294
+ events.push({
295
+ kind: 'tool_result',
296
+ id: item.id,
297
+ ok: succeeded(item),
298
+ ...(resultLines ? { lines: resultLines } : {}),
299
+ ...stamp,
300
+ });
301
+ continue;
302
+ }
303
+ if (item.type === CODEX_EXTENSION) {
304
+ // `kind` is the extension that ran (`web.search`), `query` what it
305
+ // was asked. A search query is the user's own words, already on the
306
+ // wire as the prompt that produced it.
307
+ const name = typeof item.kind === 'string' ? item.kind : 'extension';
308
+ const target = (0, transcript_tail_js_1.oneLine)(typeof item.query === 'string' ? item.query : undefined);
309
+ events.push({ kind: 'tool', id: item.id, name: (0, transcript_tail_js_1.clamp)(name), ...(target ? { target: (0, transcript_tail_js_1.clamp)(target) } : {}), ...stamp });
310
+ events.push({
311
+ kind: 'tool_result',
312
+ id: item.id,
313
+ // No failure field has been observed on an `Extension` item, and
314
+ // an empty result list is a search that found nothing rather
315
+ // than one that broke. Left as success until a failing sample
316
+ // says what a failure looks like — a guessed field reads as a
317
+ // red row on every successful search instead.
318
+ ok: true,
319
+ ...(Array.isArray(item.results) ? { lines: item.results.length } : {}),
320
+ ...stamp,
321
+ });
322
+ continue;
323
+ }
324
+ if (item.type === CODEX_FILE_CHANGE) {
325
+ // `apply_patch` is Codex's own name for the tool that writes this.
326
+ // `status` decides, as it does for a command with no exit code:
327
+ // `declined` is a patch the person refused, and it wrote nothing.
328
+ events.push({ kind: 'tool', id: item.id, name: 'apply_patch', ...fileChangeSummary(item.changes), ...stamp });
329
+ const resultLines = (0, transcript_tail_js_1.resultLinesOf)(item.stdout);
330
+ events.push({
331
+ kind: 'tool_result',
332
+ id: item.id,
333
+ ok: item.status === 'completed',
334
+ ...(resultLines ? { lines: resultLines } : {}),
335
+ ...stamp,
336
+ });
337
+ continue;
338
+ }
339
+ if (item.type === CODEX_MCP_CALL) {
340
+ // `server.tool`, the same dotted shape as `web.search`. The
341
+ // `arguments` are the MCP server's schema, not Codex's, and can
342
+ // hold anything a tool takes — a body, a URL, a token — so no
343
+ // target is drawn from them and neither is the result's text.
344
+ const server = typeof item.server === 'string' ? item.server : '';
345
+ const tool = typeof item.tool === 'string' ? item.tool : '';
346
+ const name = server && tool ? `${server}.${tool}` : tool || server || 'mcp';
347
+ const result = record(item.result);
348
+ const resultLines = (0, transcript_tail_js_1.resultLinesOf)(result?.content);
349
+ events.push({ kind: 'tool', id: item.id, name: (0, transcript_tail_js_1.clamp)(name), ...stamp });
350
+ events.push({
351
+ kind: 'tool_result',
352
+ id: item.id,
353
+ // An `error` beside `completed` is still a failure: Codex files
354
+ // a call that reached the server and was refused as completed.
355
+ // The field's name is the writer's (`McpToolCallError`); no
356
+ // failing call is in any rollout measured here. `isError` is
357
+ // MCP's own tool-level failure flag inside the result.
358
+ ok: item.status === 'completed' && item.error == null && result?.isError !== true,
359
+ ...(resultLines ? { lines: resultLines } : {}),
360
+ ...stamp,
361
+ });
362
+ continue;
363
+ }
364
+ // An item type this file has never seen falls out here, and the
365
+ // timeline is thinner by one row rather than wrong by one. Naming it
366
+ // without reading its fields would strand a call that never resolves;
367
+ // reading its fields without a sample is guessing at a vendor's format.
368
+ // Saying that it happened is neither.
369
+ opts.onUnknown?.(`codex item ${item.type}`);
370
+ }
371
+ return (0, transcript_tail_js_1.sliceSinceLastPrompt)(events, opts);
372
+ };
373
+ exports.projectCodexEntries = projectCodexEntries;