@loicngr/kobo 1.7.6 → 1.7.8

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 (110) hide show
  1. package/AGENTS.md +29 -0
  2. package/README.md +146 -4
  3. package/dist/mcp-server/kobo-tasks-server.js +27 -0
  4. package/dist/server/index.js +2 -0
  5. package/dist/server/routes/health.js +14 -0
  6. package/dist/server/routes/voice.js +149 -0
  7. package/dist/server/routes/workspaces.js +33 -9
  8. package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
  9. package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
  10. package/dist/server/services/agent/engines/codex/client.js +36 -0
  11. package/dist/server/services/agent/engines/codex/engine.js +276 -0
  12. package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
  13. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
  14. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
  15. package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
  16. package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
  17. package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
  18. package/dist/server/services/agent/engines/codex/spawn.js +27 -0
  19. package/dist/server/services/agent/engines/registry.js +2 -0
  20. package/dist/server/services/agent/orchestrator.js +1 -1
  21. package/dist/server/services/settings-service.js +125 -6
  22. package/dist/server/services/transcription-service.js +206 -0
  23. package/dist/server/utils/paths.js +7 -0
  24. package/dist/shared/codex-models.js +43 -0
  25. package/package.json +13 -10
  26. package/src/client/dist/spa/assets/ActivityFeed-CPZdjJpH.js +8 -0
  27. package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
  28. package/src/client/dist/spa/assets/{ClosePopup-D_UAdwkA.js → ClosePopup-C5JlH6Hy.js} +1 -1
  29. package/src/client/dist/spa/assets/CreatePage-CdfbFlXf.js +2 -0
  30. package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
  31. package/src/client/dist/spa/assets/{DiffViewer-CblFgn8w.js → DiffViewer-DkiP6nWz.js} +3 -3
  32. package/src/client/dist/spa/assets/HealthPage-BHGZJTgS.js +1 -0
  33. package/src/client/dist/spa/assets/{MainLayout-DhaYycak.js → MainLayout-C0tClQZl.js} +17 -17
  34. package/src/client/dist/spa/assets/{MainLayout-drolsINz.css → MainLayout-DKnTGN_Q.css} +1 -1
  35. package/src/client/dist/spa/assets/{QBadge-DWH42dbo.js → QBadge-C7r6oPSi.js} +1 -1
  36. package/src/client/dist/spa/assets/{QBtn-a6jxWjmW.js → QBtn-DEuWKHbR.js} +1 -1
  37. package/src/client/dist/spa/assets/{QCheckbox-D5jfsxLV.js → QCheckbox-BvHfXBFY.js} +1 -1
  38. package/src/client/dist/spa/assets/{QChip-ByxK0Tuf.js → QChip-erWIZgxW.js} +1 -1
  39. package/src/client/dist/spa/assets/{QExpansionItem-CH1ipL9n.js → QExpansionItem-CW6sPoP9.js} +1 -1
  40. package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +1 -0
  41. package/src/client/dist/spa/assets/{QInput-Cm5-AGQ4.js → QInput-DCJEwE8V.js} +1 -1
  42. package/src/client/dist/spa/assets/{QItemLabel-DrTxqTqV.js → QItemLabel-CHkgkZVj.js} +1 -1
  43. package/src/client/dist/spa/assets/{QItemSection-5YpFpPDm.js → QItemSection-CQUDd0Vg.js} +1 -1
  44. package/src/client/dist/spa/assets/{QList-D0FtnQJI.js → QList-BbnN_oNX.js} +1 -1
  45. package/src/client/dist/spa/assets/{QMenu-B4xMxMGd.js → QMenu-CaVfoMu6.js} +1 -1
  46. package/src/client/dist/spa/assets/{QPage-DFi3K093.js → QPage-Co2h9wd_.js} +1 -1
  47. package/src/client/dist/spa/assets/{QRadio-B3aKjCVu.js → QRadio-DJxOyOA3.js} +1 -1
  48. package/src/client/dist/spa/assets/QSpace-DKIph84L.js +1 -0
  49. package/src/client/dist/spa/assets/{QSpinnerDots-CszPQQ9J.js → QSpinnerDots-Bfl2RMy4.js} +1 -1
  50. package/src/client/dist/spa/assets/{QTabPanels-D2ks0UIA.js → QTabPanels-E66qDYmr.js} +1 -1
  51. package/src/client/dist/spa/assets/{QToggle-1-N9qWq4.js → QToggle-DNOTC_3a.js} +1 -1
  52. package/src/client/dist/spa/assets/{QTooltip-fDNzBEfN.js → QTooltip-DYey0zHV.js} +1 -1
  53. package/src/client/dist/spa/assets/{SearchPage-cZTwP4Lf.js → SearchPage-BaI3iU58.js} +1 -1
  54. package/src/client/dist/spa/assets/SettingsPage-BqBOQKeM.js +9 -0
  55. package/src/client/dist/spa/assets/SettingsPage-Zeu2cZqi.css +1 -0
  56. package/src/client/dist/spa/assets/{TouchPan-DoE24Io3.js → TouchPan-DQILDzd3.js} +1 -1
  57. package/src/client/dist/spa/assets/WorkspacePage-C9eT5LAo.css +1 -0
  58. package/src/client/dist/spa/assets/WorkspacePage-DqMyUSFG.js +4 -0
  59. package/src/client/dist/spa/assets/{build-path-tree-B1Lvvqto.js → build-path-tree-BpcCBm9A.js} +1 -1
  60. package/src/client/dist/spa/assets/{cssMode-BFLYiiEw.js → cssMode-BaeNVqUm.js} +1 -1
  61. package/src/client/dist/spa/assets/{documents-kx0vLfSG.js → documents-soWtna0O.js} +1 -1
  62. package/src/client/dist/spa/assets/{editor.api-2asmmhth.js → editor.api-DMLl_PBy.js} +1 -1
  63. package/src/client/dist/spa/assets/{editor.main-ChCYZyez.js → editor.main-D2pRsQAX.js} +3 -3
  64. package/src/client/dist/spa/assets/{AutoLoopChip-w8D77bI5.js → engineFeatures-RffgP255.js} +1 -1
  65. package/src/client/dist/spa/assets/{expand-template-CXQFkQOJ.js → expand-template-z2wIJOD2.js} +1 -1
  66. package/src/client/dist/spa/assets/{formatters-DCAQ6ANJ.js → formatters-guwb-rzl.js} +1 -1
  67. package/src/client/dist/spa/assets/{freemarker2-BaBL9E9G.js → freemarker2-Bh6ItnVy.js} +1 -1
  68. package/src/client/dist/spa/assets/{handlebars-BxDour4L.js → handlebars-D8OXeysi.js} +1 -1
  69. package/src/client/dist/spa/assets/{html-C6hnkfIL.js → html-9Y1AHhvw.js} +1 -1
  70. package/src/client/dist/spa/assets/{htmlMode-9zT3-dmz.js → htmlMode-z00se0fQ.js} +1 -1
  71. package/src/client/dist/spa/assets/i18n-C-VMW7h5.js +1 -0
  72. package/src/client/dist/spa/assets/index-BLlWqEZC.js +2 -0
  73. package/src/client/dist/spa/assets/{javascript-C3YjvKbE.js → javascript-D0LSb7WU.js} +1 -1
  74. package/src/client/dist/spa/assets/{jsonMode-DcJDgMzf.js → jsonMode-BSmyaoX3.js} +1 -1
  75. package/src/client/dist/spa/assets/{liquid-CsT8SjJM.js → liquid-BsY5UXNl.js} +1 -1
  76. package/src/client/dist/spa/assets/{mdx-CT3yVSyc.js → mdx-BUcXih4e.js} +1 -1
  77. package/src/client/dist/spa/assets/{monaco.contribution-DKGNz1oQ.js → monaco.contribution-DrpufOT3.js} +2 -2
  78. package/src/client/dist/spa/assets/{notifications-OnPq4FrH.js → notifications-C255ApfS.js} +1 -1
  79. package/src/client/dist/spa/assets/permissionModes-BocOmzU8.js +1 -0
  80. package/src/client/dist/spa/assets/{purify.es-CPieV82n.js → purify.es-aV6SU8N4.js} +1 -1
  81. package/src/client/dist/spa/assets/{python-Ca5miKgj.js → python-C0PoB7M8.js} +1 -1
  82. package/src/client/dist/spa/assets/{razor-7qzusGRc.js → razor-Bu0-fwxD.js} +1 -1
  83. package/src/client/dist/spa/assets/{render-chat-markdown-Bqq2G-yI.js → render-chat-markdown-DALCdDVE.js} +1 -1
  84. package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +1 -0
  85. package/src/client/dist/spa/assets/{tsMode-BdvO8jZ2.js → tsMode-Blc1d2dp.js} +1 -1
  86. package/src/client/dist/spa/assets/{typescript-BfVNzhgs.js → typescript-CV4ME9fo.js} +1 -1
  87. package/src/client/dist/spa/assets/{use-checkbox-D7zmRxGI.js → use-checkbox-y_fOkYZN.js} +1 -1
  88. package/src/client/dist/spa/assets/{use-id-CuaR1RiE.js → use-id-_7wiRcgb.js} +1 -1
  89. package/src/client/dist/spa/assets/{use-panel-D-8nAQns.js → use-panel-DCPiSURS.js} +1 -1
  90. package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +1 -0
  91. package/src/client/dist/spa/assets/{vue-i18n-BcfTCFFS.js → vue-i18n-DI-gS-CC.js} +1 -1
  92. package/src/client/dist/spa/assets/{xml-DGNXGqXL.js → xml-DLYRBBbI.js} +1 -1
  93. package/src/client/dist/spa/assets/{yaml-CtAtOyt5.js → yaml-QIBjI5Dl.js} +1 -1
  94. package/src/client/dist/spa/index.html +12 -12
  95. package/src/mcp-server/kobo-tasks-server.ts +27 -0
  96. package/src/client/dist/spa/assets/ActivityFeed-BboSPm4b.js +0 -7
  97. package/src/client/dist/spa/assets/CreatePage-BDObLDJc.js +0 -2
  98. package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +0 -1
  99. package/src/client/dist/spa/assets/HealthPage-CBSw7e5q.js +0 -1
  100. package/src/client/dist/spa/assets/QIcon-BJuyqdsT.js +0 -1
  101. package/src/client/dist/spa/assets/QSpace-CLtL3aPy.js +0 -1
  102. package/src/client/dist/spa/assets/SettingsPage-C1efO0VM.js +0 -1
  103. package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +0 -1
  104. package/src/client/dist/spa/assets/WorkspacePage-3jcof896.js +0 -4
  105. package/src/client/dist/spa/assets/WorkspacePage-CCtIrBiR.css +0 -1
  106. package/src/client/dist/spa/assets/i18n-CLY0XI9-.js +0 -1
  107. package/src/client/dist/spa/assets/index-D6wj_wQ9.js +0 -2
  108. package/src/client/dist/spa/assets/models-BsjWUKqM.js +0 -1
  109. package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +0 -1
  110. package/src/client/dist/spa/assets/use-quasar-Sdcq6zzV.js +0 -1
@@ -0,0 +1,473 @@
1
+ import { nanoid } from 'nanoid';
2
+ // ── QUOTA_PATTERN ─────────────────────────────────────────────────────────────
3
+ //
4
+ // Loose enough to catch wording drift across OpenAI/Codex quota messages.
5
+ // Patterns covered:
6
+ // - "rate limit reached" / "rate_limit exceeded"
7
+ // - "quota exceeded" / "insufficient_quota" / "insufficient quota"
8
+ // - "out of extra usage" / "out of usage"
9
+ // - "usage limit exceeded"
10
+ export const QUOTA_PATTERN = /rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
11
+ export function createMapperState(opts) {
12
+ return {
13
+ sessionStartedEmitted: false,
14
+ openMessages: new Map(),
15
+ sawErrorResult: false,
16
+ sawTurnInterrupted: false,
17
+ quotaErrorEmitted: false,
18
+ // Optional explicit prefix for deterministic testing; production code path
19
+ // generates a fresh nanoid-based prefix per engine.start() call.
20
+ sessionPrefix: opts?.sessionPrefix ?? `cdx_${nanoid(10)}`,
21
+ };
22
+ }
23
+ // ── tryEmitQuota ──────────────────────────────────────────────────────────────
24
+ /**
25
+ * Emit an error/quota event exactly once per stream, regardless of which
26
+ * surface detected the quota condition. Also sets sawErrorResult so the engine
27
+ * can surface session:ended with reason='error'.
28
+ *
29
+ * Exported so the engine's stderr path can share the same one-shot guard.
30
+ */
31
+ export function tryEmitQuota(state, emit, message) {
32
+ if (state.quotaErrorEmitted)
33
+ return;
34
+ state.quotaErrorEmitted = true;
35
+ state.sawErrorResult = true;
36
+ emit({ kind: 'error', category: 'quota', message });
37
+ }
38
+ // ── Internal helpers ──────────────────────────────────────────────────────────
39
+ /** Push-array variant of tryEmitQuota for use inside handler functions. */
40
+ function tryEmitQuotaInline(state, events, message) {
41
+ tryEmitQuota(state, (ev) => events.push(ev), message);
42
+ }
43
+ /**
44
+ * Scope item ids by a per-session prefix. Codex restarts item numbering
45
+ * (item_0, item_1, …) on each resume, so a fresh prefix prevents the client
46
+ * store from deduping the second turn's item_0 against the first.
47
+ */
48
+ function scopedId(state, itemId) {
49
+ return `${state.sessionPrefix}_${itemId}`;
50
+ }
51
+ // ── Public API ────────────────────────────────────────────────────────────────
52
+ /**
53
+ * Emit session:started exactly once for this session.
54
+ * Idempotent — subsequent calls for the same threadId are no-ops.
55
+ */
56
+ export function emitSessionStarted(threadId, state) {
57
+ if (state.sessionStartedEmitted)
58
+ return [];
59
+ state.sessionStartedEmitted = true;
60
+ state.sessionId = threadId;
61
+ return [{ kind: 'session:started', engineSessionId: threadId }];
62
+ }
63
+ /**
64
+ * Handle item/started notifications from the app-server.
65
+ */
66
+ export function handleItemStarted(item, state) {
67
+ const events = [];
68
+ if (item.type === 'agentMessage') {
69
+ const text = item.text ?? '';
70
+ const messageId = scopedId(state, item.id);
71
+ state.openMessages.set(messageId, { sawText: text.length > 0 });
72
+ events.push({ kind: 'message:text', messageId, text, streaming: true });
73
+ return events;
74
+ }
75
+ if (item.type === 'commandExecution') {
76
+ events.push({
77
+ kind: 'tool:call',
78
+ messageId: '',
79
+ toolCallId: scopedId(state, item.id),
80
+ name: 'Bash',
81
+ input: { command: item.command },
82
+ });
83
+ return events;
84
+ }
85
+ if (item.type === 'fileChange') {
86
+ events.push({
87
+ kind: 'tool:call',
88
+ messageId: '',
89
+ toolCallId: scopedId(state, item.id),
90
+ name: 'Edit',
91
+ input: codexFileChangeToInput(item.changes ?? []),
92
+ });
93
+ return events;
94
+ }
95
+ if (item.type === 'mcpToolCall') {
96
+ const name = `mcp__${item.server}__${item.tool}`;
97
+ events.push({
98
+ kind: 'tool:call',
99
+ messageId: '',
100
+ toolCallId: scopedId(state, item.id),
101
+ name,
102
+ input: item.arguments,
103
+ });
104
+ return events;
105
+ }
106
+ if (item.type === 'webSearch') {
107
+ events.push({
108
+ kind: 'tool:call',
109
+ messageId: '',
110
+ toolCallId: scopedId(state, item.id),
111
+ name: 'WebSearch',
112
+ input: { query: item.query },
113
+ });
114
+ return events;
115
+ }
116
+ if (item.type === 'collabAgentToolCall') {
117
+ const toolCallId = scopedId(state, item.id);
118
+ events.push({
119
+ kind: 'tool:call',
120
+ messageId: '',
121
+ toolCallId,
122
+ name: 'Task',
123
+ input: {
124
+ codex_tool: item.tool,
125
+ description: item.prompt ?? item.tool,
126
+ prompt: item.prompt,
127
+ model: item.model,
128
+ sender_thread_id: item.senderThreadId,
129
+ receiver_thread_ids: item.receiverThreadIds,
130
+ },
131
+ });
132
+ events.push({
133
+ kind: 'subagent:progress',
134
+ toolCallId,
135
+ status: 'running',
136
+ description: item.prompt ?? item.tool,
137
+ taskType: item.tool,
138
+ });
139
+ return events;
140
+ }
141
+ if (item.type === 'dynamicToolCall') {
142
+ const name = item.namespace ? `${item.namespace}__${item.tool}` : item.tool;
143
+ events.push({
144
+ kind: 'tool:call',
145
+ messageId: '',
146
+ toolCallId: scopedId(state, item.id),
147
+ name,
148
+ input: item.arguments ?? {},
149
+ });
150
+ return events;
151
+ }
152
+ if (item.type === 'imageView') {
153
+ events.push({
154
+ kind: 'tool:call',
155
+ messageId: '',
156
+ toolCallId: scopedId(state, item.id),
157
+ name: 'Read',
158
+ input: { file_path: item.path },
159
+ });
160
+ return events;
161
+ }
162
+ if (item.type === 'imageGeneration') {
163
+ events.push({
164
+ kind: 'tool:call',
165
+ messageId: '',
166
+ toolCallId: scopedId(state, item.id),
167
+ name: 'ImageGeneration',
168
+ input: { revisedPrompt: item.revisedPrompt },
169
+ });
170
+ return events;
171
+ }
172
+ if (item.type === 'enteredReviewMode' || item.type === 'exitedReviewMode') {
173
+ const label = item.type === 'enteredReviewMode' ? 'review:start' : 'review:end';
174
+ events.push({
175
+ kind: 'message:thinking',
176
+ messageId: scopedId(state, item.id),
177
+ text: `[${label}] ${item.review}`,
178
+ });
179
+ return events;
180
+ }
181
+ if (item.type === 'contextCompaction') {
182
+ events.push({ kind: 'session:compacted' });
183
+ return events;
184
+ }
185
+ return events;
186
+ }
187
+ /**
188
+ * Handle item/completed notifications from the app-server.
189
+ */
190
+ export function handleItemCompleted(item, state) {
191
+ const events = [];
192
+ if (item.type === 'agentMessage') {
193
+ const text = item.text ?? '';
194
+ const messageId = scopedId(state, item.id);
195
+ const open = state.openMessages.get(messageId);
196
+ // Skip the final emit when deltas already streamed the text — the client
197
+ // accumulates per-delta and would double-append on a fresh push.
198
+ const alreadyStreamed = open?.sawText === true;
199
+ if (!alreadyStreamed) {
200
+ events.push({ kind: 'message:text', messageId, text, streaming: false });
201
+ }
202
+ if (text.includes('[BRAINSTORM_COMPLETE]')) {
203
+ events.push({ kind: 'session:brainstorm-complete' });
204
+ }
205
+ if (QUOTA_PATTERN.test(text)) {
206
+ tryEmitQuotaInline(state, events, text);
207
+ }
208
+ events.push({ kind: 'message:end', messageId });
209
+ state.openMessages.delete(messageId);
210
+ return events;
211
+ }
212
+ if (item.type === 'reasoning') {
213
+ const text = [...(item.summary ?? []), ...(item.content ?? [])].join('\n');
214
+ events.push({ kind: 'message:thinking', messageId: scopedId(state, item.id), text });
215
+ return events;
216
+ }
217
+ if (item.type === 'commandExecution') {
218
+ events.push({
219
+ kind: 'tool:result',
220
+ toolCallId: scopedId(state, item.id),
221
+ output: { aggregated_output: item.aggregatedOutput ?? '', exit_code: item.exitCode },
222
+ isError: item.status === 'failed',
223
+ });
224
+ return events;
225
+ }
226
+ if (item.type === 'fileChange') {
227
+ events.push({
228
+ kind: 'tool:result',
229
+ toolCallId: scopedId(state, item.id),
230
+ output: { changes: item.changes ?? [], status: item.status },
231
+ isError: item.status === 'failed',
232
+ });
233
+ return events;
234
+ }
235
+ if (item.type === 'mcpToolCall') {
236
+ events.push({
237
+ kind: 'tool:result',
238
+ toolCallId: scopedId(state, item.id),
239
+ output: item.result ?? item.error ?? null,
240
+ isError: item.status === 'failed',
241
+ });
242
+ return events;
243
+ }
244
+ if (item.type === 'webSearch') {
245
+ events.push({
246
+ kind: 'tool:result',
247
+ toolCallId: scopedId(state, item.id),
248
+ output: { query: item.query },
249
+ isError: false,
250
+ });
251
+ return events;
252
+ }
253
+ if (item.type === 'collabAgentToolCall') {
254
+ const toolCallId = scopedId(state, item.id);
255
+ events.push({
256
+ kind: 'tool:result',
257
+ toolCallId,
258
+ output: {
259
+ codex_tool: item.tool,
260
+ status: item.status,
261
+ agents_states: item.agentsStates,
262
+ },
263
+ isError: item.status === 'failed',
264
+ });
265
+ events.push({
266
+ kind: 'subagent:progress',
267
+ toolCallId,
268
+ status: 'done',
269
+ description: item.prompt ?? item.tool,
270
+ taskType: item.tool,
271
+ });
272
+ return events;
273
+ }
274
+ if (item.type === 'dynamicToolCall') {
275
+ events.push({
276
+ kind: 'tool:result',
277
+ toolCallId: scopedId(state, item.id),
278
+ output: { contentItems: item.contentItems ?? [], success: item.success, durationMs: item.durationMs },
279
+ isError: item.status === 'failed' || item.success === false,
280
+ });
281
+ return events;
282
+ }
283
+ if (item.type === 'imageView') {
284
+ events.push({
285
+ kind: 'tool:result',
286
+ toolCallId: scopedId(state, item.id),
287
+ output: { file_path: item.path },
288
+ isError: false,
289
+ });
290
+ return events;
291
+ }
292
+ if (item.type === 'imageGeneration') {
293
+ events.push({
294
+ kind: 'tool:result',
295
+ toolCallId: scopedId(state, item.id),
296
+ output: { savedPath: item.savedPath, result: item.result, revisedPrompt: item.revisedPrompt },
297
+ isError: false,
298
+ });
299
+ return events;
300
+ }
301
+ if (item.type === 'plan') {
302
+ // Codex's `plan` is a markdown blob, not a structured list — parse bullets.
303
+ events.push({
304
+ kind: 'tool:call',
305
+ messageId: '',
306
+ toolCallId: scopedId(state, item.id),
307
+ name: 'TodoWrite',
308
+ input: { todos: parseCodexPlanText(item.text) },
309
+ });
310
+ return events;
311
+ }
312
+ if (item.type === 'error') {
313
+ if (QUOTA_PATTERN.test(item.message)) {
314
+ tryEmitQuotaInline(state, events, item.message);
315
+ }
316
+ else {
317
+ state.sawErrorResult = true;
318
+ events.push({ kind: 'error', category: 'other', message: item.message });
319
+ }
320
+ return events;
321
+ }
322
+ return events;
323
+ }
324
+ /**
325
+ * Map `item/agentMessage/delta` to a `message:text` increment.
326
+ * Marks `sawText` so the eventual `item/completed` doesn't re-emit the full
327
+ * text (the client accumulates per-delta — re-emit would double-append).
328
+ */
329
+ export function handleAgentMessageDelta(delta, state) {
330
+ const messageId = scopedId(state, delta.itemId);
331
+ if (delta.delta.length > 0) {
332
+ const open = state.openMessages.get(messageId);
333
+ if (open) {
334
+ open.sawText = true;
335
+ }
336
+ else {
337
+ state.openMessages.set(messageId, { sawText: true });
338
+ }
339
+ }
340
+ return [{ kind: 'message:text', messageId, text: delta.delta, streaming: true }];
341
+ }
342
+ /**
343
+ * Handle `turn/completed`. Emits an `error` on `status=failed`, flags
344
+ * `sawTurnInterrupted` on `status=interrupted`. Token usage arrives on the
345
+ * separate `thread/tokenUsage/updated` notification.
346
+ */
347
+ export function handleTurnCompleted(n, state) {
348
+ const events = [];
349
+ const { status, error } = n.turn;
350
+ if (status === 'failed') {
351
+ const msg = error?.message ?? 'turn failed';
352
+ if (QUOTA_PATTERN.test(msg)) {
353
+ tryEmitQuotaInline(state, events, msg);
354
+ }
355
+ else {
356
+ state.sawErrorResult = true;
357
+ events.push({ kind: 'error', category: 'other', message: msg });
358
+ }
359
+ }
360
+ else if (status === 'interrupted') {
361
+ // Engine reads this to emit `session:ended reason='killed'` when the
362
+ // turn was interrupted server-side without throwing.
363
+ state.sawTurnInterrupted = true;
364
+ }
365
+ return events;
366
+ }
367
+ /**
368
+ * Map `account/rateLimits/updated` to a `rate_limit` AgentEvent for the
369
+ * QuotaFooter. Translates Codex's unix-seconds `resetsAt` to ISO-8601.
370
+ */
371
+ export function handleRateLimitsUpdated(payload, state) {
372
+ const p = payload;
373
+ const rl = p?.rateLimits;
374
+ if (!rl)
375
+ return [];
376
+ const buckets = [];
377
+ const toBucket = (id, win) => {
378
+ if (!win)
379
+ return;
380
+ const bucket = {
381
+ id,
382
+ usedPct: Math.max(0, Math.min(100, win.usedPercent)),
383
+ };
384
+ if (rl.limitName)
385
+ bucket.label = rl.limitName;
386
+ if (typeof win.resetsAt === 'number' && Number.isFinite(win.resetsAt)) {
387
+ bucket.resetsAt = new Date(win.resetsAt * 1000).toISOString();
388
+ }
389
+ buckets.push(bucket);
390
+ };
391
+ toBucket('primary', rl.primary ?? null);
392
+ toBucket('secondary', rl.secondary ?? null);
393
+ if (buckets.length === 0)
394
+ return [];
395
+ const reached = typeof rl.rateLimitReachedType === 'string' && rl.rateLimitReachedType.length > 0;
396
+ const maxPct = Math.max(...buckets.map((b) => b.usedPct));
397
+ const status = reached
398
+ ? 'rejected'
399
+ : maxPct >= 80
400
+ ? 'allowed_warning'
401
+ : 'allowed';
402
+ const events = [{ kind: 'rate_limit', info: { buckets, status } }];
403
+ if (reached) {
404
+ tryEmitQuotaInline(state, events, `Codex rate limit reached: ${rl.rateLimitReachedType}`);
405
+ }
406
+ return events;
407
+ }
408
+ // ── FileChange normalisation ──────────────────────────────────────────────────
409
+ /**
410
+ * Normalise Codex's `fileChange` to the Claude-style `tool:call` input the
411
+ * renderer expects (`file_path` + unified `diff`). Without `file_path` the
412
+ * UI falls back to dumping raw JSON.
413
+ */
414
+ export function codexFileChangeToInput(changes) {
415
+ if (changes.length === 0) {
416
+ return { file_path: '', diff: '', changes: [] };
417
+ }
418
+ const first = changes[0];
419
+ const kind = first.kind.type;
420
+ const movePath = first.kind.type === 'update' ? first.kind.move_path : null;
421
+ return {
422
+ file_path: first.path,
423
+ diff: first.diff,
424
+ change_kind: kind,
425
+ ...(movePath != null ? { move_path: movePath } : {}),
426
+ changes,
427
+ };
428
+ }
429
+ // ── Plan parsing ──────────────────────────────────────────────────────────────
430
+ const BULLET_LINE = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/;
431
+ const HEADING_LINE = /^\s*#{1,6}\s/;
432
+ /**
433
+ * Split a markdown `plan` blob into individual todo items.
434
+ * Bullets (`-`, `*`, `+`, `1.`, `1)`) become items; headings are dropped;
435
+ * untagged continuation lines fold into the previous item.
436
+ */
437
+ export function parseCodexPlanText(text) {
438
+ const lines = text.split('\n');
439
+ const items = [];
440
+ let current = null;
441
+ const flush = () => {
442
+ if (current != null) {
443
+ const trimmed = current.trim();
444
+ if (trimmed.length > 0)
445
+ items.push(trimmed);
446
+ current = null;
447
+ }
448
+ };
449
+ for (const raw of lines) {
450
+ if (HEADING_LINE.test(raw)) {
451
+ flush();
452
+ continue;
453
+ }
454
+ const match = raw.match(BULLET_LINE);
455
+ if (match) {
456
+ flush();
457
+ current = match[1].trim();
458
+ continue;
459
+ }
460
+ if (current != null) {
461
+ const cont = raw.trim();
462
+ current = cont.length > 0 ? `${current} ${cont}` : current;
463
+ }
464
+ }
465
+ flush();
466
+ if (items.length === 0) {
467
+ const trimmed = text.trim();
468
+ if (trimmed.length === 0)
469
+ return [];
470
+ return [{ content: trimmed, status: 'pending' }];
471
+ }
472
+ return items.map((content) => ({ content, status: 'pending' }));
473
+ }
@@ -0,0 +1,60 @@
1
+ import { createJsonRpcTransport } from './transport.js';
2
+ export function createJsonRpcPeer(opts) {
3
+ let nextId = 1;
4
+ const pending = new Map();
5
+ const transport = createJsonRpcTransport({
6
+ stdin: opts.stdin,
7
+ stdout: opts.stdout,
8
+ onError: opts.onError ?? (() => { }),
9
+ onMessage(msg) {
10
+ // Response to one of our requests
11
+ if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
12
+ const slot = pending.get(msg.id);
13
+ if (!slot)
14
+ return;
15
+ pending.delete(msg.id);
16
+ if (msg.error) {
17
+ slot.reject(new Error(`JSON-RPC error ${msg.error.code}: ${msg.error.message}`));
18
+ }
19
+ else {
20
+ slot.resolve(msg.result);
21
+ }
22
+ return;
23
+ }
24
+ // Server-initiated request (has id + method)
25
+ if (msg.id != null && msg.method) {
26
+ opts.onServerRequest(msg.id, msg.method, msg.params);
27
+ return;
28
+ }
29
+ // Notification (no id)
30
+ if (msg.method) {
31
+ opts.onNotification(msg.method, msg.params);
32
+ }
33
+ },
34
+ });
35
+ return {
36
+ request(method, params) {
37
+ const id = nextId++;
38
+ return new Promise((resolve, reject) => {
39
+ pending.set(id, { resolve: (v) => resolve(v), reject });
40
+ transport.send({ jsonrpc: '2.0', id, method, params });
41
+ });
42
+ },
43
+ notify(method, params) {
44
+ transport.send({ jsonrpc: '2.0', method, params });
45
+ },
46
+ respond(id, result) {
47
+ transport.send({ jsonrpc: '2.0', id, result });
48
+ },
49
+ respondError(id, code, message) {
50
+ transport.send({ jsonrpc: '2.0', id, error: { code, message } });
51
+ },
52
+ close() {
53
+ transport.close();
54
+ const err = new Error('peer closed');
55
+ for (const slot of pending.values())
56
+ slot.reject(err);
57
+ pending.clear();
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,31 @@
1
+ export function createJsonRpcTransport(opts) {
2
+ let buffer = '';
3
+ opts.stdout.setEncoding?.('utf-8');
4
+ opts.stdout.on('data', (chunk) => {
5
+ buffer += chunk;
6
+ let nl = buffer.indexOf('\n');
7
+ while (nl >= 0) {
8
+ const line = buffer.slice(0, nl).trim();
9
+ buffer = buffer.slice(nl + 1);
10
+ nl = buffer.indexOf('\n');
11
+ if (!line)
12
+ continue;
13
+ try {
14
+ const parsed = JSON.parse(line);
15
+ opts.onMessage(parsed);
16
+ }
17
+ catch (err) {
18
+ opts.onError(err instanceof Error ? err : new Error(String(err)));
19
+ }
20
+ }
21
+ });
22
+ opts.stdout.on('error', opts.onError);
23
+ return {
24
+ send(msg) {
25
+ opts.stdin.write(`${JSON.stringify(msg)}\n`);
26
+ },
27
+ close() {
28
+ opts.stdin.end();
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,81 @@
1
+ const CODEX_KOBO_MCP_BRIEF = [
2
+ '[Kōbō MCP] This workspace exposes a dedicated MCP server named `kobo-tasks`. The Codex CLI surfaces its tools under the literal name `mcp__kobo-tasks__<tool>` — always use that full form when invoking them.',
3
+ 'Plan mode: when running with a read-only sandbox, the read-only restriction applies to MCP tools too. Reads (`mcp__kobo-tasks__list_*`, `mcp__kobo-tasks__read_document`, `mcp__kobo-tasks__search_codebase`, `mcp__kobo-tasks__get_*`) are fine; mutations (`mcp__kobo-tasks__mark_task_done`, `mcp__kobo-tasks__log_thought`, `mcp__kobo-tasks__set_workspace_status`) must wait until the plan is approved.',
4
+ 'Conventions — read these BEFORE starting work, not as a fallback:',
5
+ '• `mcp__kobo-tasks__list_tasks` first on any non-trivial turn, then `mcp__kobo-tasks__mark_task_done` as each item completes.',
6
+ '• `mcp__kobo-tasks__list_documents` / `mcp__kobo-tasks__read_document` to discover existing plans and specs under docs/ and .ai/thoughts/ before writing new ones.',
7
+ '• `mcp__kobo-tasks__log_thought` to persist notable decisions to `.ai/thoughts/<date>-<slug>.md`.',
8
+ '• `mcp__kobo-tasks__search_codebase` to recall prior chat history (conversations, not source — use shell tools for source).',
9
+ '• `mcp__kobo-tasks__get_workspace_info` / `mcp__kobo-tasks__get_git_info` / `mcp__kobo-tasks__get_notion_ticket` for context.',
10
+ '• `mcp__kobo-tasks__set_workspace_status` when the mission is done / blocked / idle.',
11
+ '• `mcp__kobo-tasks__schedule_wakeup` / `mcp__kobo-tasks__cancel_wakeup` to schedule (or cancel) a follow-up session.',
12
+ 'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
13
+ ].join('\n');
14
+ export function buildCodexOptions(input) {
15
+ const isResume = input.resumeFromEngineSessionId !== undefined;
16
+ const threadParams = {
17
+ cwd: input.workingDir,
18
+ experimentalRawEvents: false,
19
+ persistExtendedHistory: false,
20
+ };
21
+ // Permission mode → (sandbox, approvalPolicy)
22
+ switch (input.agentPermissionMode) {
23
+ case 'plan':
24
+ threadParams.sandbox = 'read-only';
25
+ threadParams.approvalPolicy = 'never';
26
+ break;
27
+ case 'bypass':
28
+ threadParams.sandbox = 'workspace-write';
29
+ threadParams.approvalPolicy = 'never';
30
+ break;
31
+ case 'strict':
32
+ threadParams.sandbox = 'workspace-write';
33
+ threadParams.approvalPolicy = 'on-request';
34
+ break;
35
+ case 'interactive':
36
+ threadParams.sandbox = 'workspace-write';
37
+ threadParams.approvalPolicy = 'unless-trusted';
38
+ break;
39
+ }
40
+ // Model: omit when undefined or 'auto', let Codex use its default
41
+ if (input.model && input.model !== 'auto') {
42
+ threadParams.model = input.model;
43
+ }
44
+ // Effort: omit when undefined or 'auto'
45
+ if (input.effort && input.effort !== 'auto') {
46
+ threadParams.modelReasoningEffort = input.effort;
47
+ }
48
+ // `default_tools_approval_mode: 'auto'` pre-approves the namespace —
49
+ // without it Codex blocks every MCP tool call with "user cancelled".
50
+ if (input.mcpServers && input.mcpServers.length > 0) {
51
+ const mcpServersConfig = {};
52
+ for (const srv of input.mcpServers) {
53
+ mcpServersConfig[srv.name] = {
54
+ command: srv.command,
55
+ args: srv.args,
56
+ env: srv.env,
57
+ default_tools_approval_mode: 'auto',
58
+ };
59
+ }
60
+ threadParams.config = { mcp_servers: mcpServersConfig };
61
+ }
62
+ const effectivePrompt = isResume ? input.prompt : `${CODEX_KOBO_MCP_BRIEF}\n\n${input.prompt}`;
63
+ // Always emit collaborationMode — it's sticky server-side, so omitting it
64
+ // would leave a resumed thread stuck in the previous turn's mode. Plan also
65
+ // gates the `request_user_input` internal tool. Settings echo the resolved
66
+ // model/effort because collaborationMode takes precedence over them.
67
+ const collaborationMode = {
68
+ mode: input.agentPermissionMode === 'plan' ? 'plan' : 'default',
69
+ settings: {
70
+ model: threadParams.model ?? 'auto',
71
+ reasoning_effort: threadParams.modelReasoningEffort ?? null,
72
+ developer_instructions: null,
73
+ },
74
+ };
75
+ return {
76
+ threadParams,
77
+ input: [{ type: 'text', text: effectivePrompt }],
78
+ isResume,
79
+ collaborationMode,
80
+ };
81
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Codex app-server JSON-RPC protocol — minimal type subset for Kōbō.
3
+ *
4
+ * Hand-written from the canonical generated bindings in
5
+ * docs/superpowers/plans/codex-generated-bindings/v2/
6
+ * and verified against the live wire capture (2026-05-11).
7
+ *
8
+ * Only the types needed by the Kōbō engine are included here.
9
+ * The full generated set (85+ files) is preserved in docs/ for reference.
10
+ */
11
+ export {};