@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.
- package/AGENTS.md +29 -0
- package/README.md +146 -4
- package/dist/mcp-server/kobo-tasks-server.js +27 -0
- package/dist/server/index.js +2 -0
- package/dist/server/routes/health.js +14 -0
- package/dist/server/routes/voice.js +149 -0
- package/dist/server/routes/workspaces.js +33 -9
- package/dist/server/services/agent/engines/claude-code/capabilities.js +7 -0
- package/dist/server/services/agent/engines/codex/capabilities.js +18 -0
- package/dist/server/services/agent/engines/codex/client.js +36 -0
- package/dist/server/services/agent/engines/codex/engine.js +276 -0
- package/dist/server/services/agent/engines/codex/event-mapper.js +473 -0
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +60 -0
- package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +31 -0
- package/dist/server/services/agent/engines/codex/options-builder.js +81 -0
- package/dist/server/services/agent/engines/codex/protocol/types.js +11 -0
- package/dist/server/services/agent/engines/codex/server-requests.js +99 -0
- package/dist/server/services/agent/engines/codex/spawn.js +27 -0
- package/dist/server/services/agent/engines/registry.js +2 -0
- package/dist/server/services/agent/orchestrator.js +1 -1
- package/dist/server/services/settings-service.js +125 -6
- package/dist/server/services/transcription-service.js +206 -0
- package/dist/server/utils/paths.js +7 -0
- package/dist/shared/codex-models.js +43 -0
- package/package.json +13 -10
- package/src/client/dist/spa/assets/ActivityFeed-CPZdjJpH.js +8 -0
- package/src/client/dist/spa/assets/{ActivityFeed-tE4LVYck.css → ActivityFeed-WjiQ9716.css} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-D_UAdwkA.js → ClosePopup-C5JlH6Hy.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-CdfbFlXf.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-CblFgn8w.js → DiffViewer-DkiP6nWz.js} +3 -3
- package/src/client/dist/spa/assets/HealthPage-BHGZJTgS.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-DhaYycak.js → MainLayout-C0tClQZl.js} +17 -17
- package/src/client/dist/spa/assets/{MainLayout-drolsINz.css → MainLayout-DKnTGN_Q.css} +1 -1
- package/src/client/dist/spa/assets/{QBadge-DWH42dbo.js → QBadge-C7r6oPSi.js} +1 -1
- package/src/client/dist/spa/assets/{QBtn-a6jxWjmW.js → QBtn-DEuWKHbR.js} +1 -1
- package/src/client/dist/spa/assets/{QCheckbox-D5jfsxLV.js → QCheckbox-BvHfXBFY.js} +1 -1
- package/src/client/dist/spa/assets/{QChip-ByxK0Tuf.js → QChip-erWIZgxW.js} +1 -1
- package/src/client/dist/spa/assets/{QExpansionItem-CH1ipL9n.js → QExpansionItem-CW6sPoP9.js} +1 -1
- package/src/client/dist/spa/assets/QIcon-qfJNZLIW.js +1 -0
- package/src/client/dist/spa/assets/{QInput-Cm5-AGQ4.js → QInput-DCJEwE8V.js} +1 -1
- package/src/client/dist/spa/assets/{QItemLabel-DrTxqTqV.js → QItemLabel-CHkgkZVj.js} +1 -1
- package/src/client/dist/spa/assets/{QItemSection-5YpFpPDm.js → QItemSection-CQUDd0Vg.js} +1 -1
- package/src/client/dist/spa/assets/{QList-D0FtnQJI.js → QList-BbnN_oNX.js} +1 -1
- package/src/client/dist/spa/assets/{QMenu-B4xMxMGd.js → QMenu-CaVfoMu6.js} +1 -1
- package/src/client/dist/spa/assets/{QPage-DFi3K093.js → QPage-Co2h9wd_.js} +1 -1
- package/src/client/dist/spa/assets/{QRadio-B3aKjCVu.js → QRadio-DJxOyOA3.js} +1 -1
- package/src/client/dist/spa/assets/QSpace-DKIph84L.js +1 -0
- package/src/client/dist/spa/assets/{QSpinnerDots-CszPQQ9J.js → QSpinnerDots-Bfl2RMy4.js} +1 -1
- package/src/client/dist/spa/assets/{QTabPanels-D2ks0UIA.js → QTabPanels-E66qDYmr.js} +1 -1
- package/src/client/dist/spa/assets/{QToggle-1-N9qWq4.js → QToggle-DNOTC_3a.js} +1 -1
- package/src/client/dist/spa/assets/{QTooltip-fDNzBEfN.js → QTooltip-DYey0zHV.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-cZTwP4Lf.js → SearchPage-BaI3iU58.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-BqBOQKeM.js +9 -0
- package/src/client/dist/spa/assets/SettingsPage-Zeu2cZqi.css +1 -0
- package/src/client/dist/spa/assets/{TouchPan-DoE24Io3.js → TouchPan-DQILDzd3.js} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-C9eT5LAo.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-DqMyUSFG.js +4 -0
- package/src/client/dist/spa/assets/{build-path-tree-B1Lvvqto.js → build-path-tree-BpcCBm9A.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-BFLYiiEw.js → cssMode-BaeNVqUm.js} +1 -1
- package/src/client/dist/spa/assets/{documents-kx0vLfSG.js → documents-soWtna0O.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-2asmmhth.js → editor.api-DMLl_PBy.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-ChCYZyez.js → editor.main-D2pRsQAX.js} +3 -3
- package/src/client/dist/spa/assets/{AutoLoopChip-w8D77bI5.js → engineFeatures-RffgP255.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-CXQFkQOJ.js → expand-template-z2wIJOD2.js} +1 -1
- package/src/client/dist/spa/assets/{formatters-DCAQ6ANJ.js → formatters-guwb-rzl.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-BaBL9E9G.js → freemarker2-Bh6ItnVy.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BxDour4L.js → handlebars-D8OXeysi.js} +1 -1
- package/src/client/dist/spa/assets/{html-C6hnkfIL.js → html-9Y1AHhvw.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-9zT3-dmz.js → htmlMode-z00se0fQ.js} +1 -1
- package/src/client/dist/spa/assets/i18n-C-VMW7h5.js +1 -0
- package/src/client/dist/spa/assets/index-BLlWqEZC.js +2 -0
- package/src/client/dist/spa/assets/{javascript-C3YjvKbE.js → javascript-D0LSb7WU.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-DcJDgMzf.js → jsonMode-BSmyaoX3.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-CsT8SjJM.js → liquid-BsY5UXNl.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-CT3yVSyc.js → mdx-BUcXih4e.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DKGNz1oQ.js → monaco.contribution-DrpufOT3.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-OnPq4FrH.js → notifications-C255ApfS.js} +1 -1
- package/src/client/dist/spa/assets/permissionModes-BocOmzU8.js +1 -0
- package/src/client/dist/spa/assets/{purify.es-CPieV82n.js → purify.es-aV6SU8N4.js} +1 -1
- package/src/client/dist/spa/assets/{python-Ca5miKgj.js → python-C0PoB7M8.js} +1 -1
- package/src/client/dist/spa/assets/{razor-7qzusGRc.js → razor-Bu0-fwxD.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-Bqq2G-yI.js → render-chat-markdown-DALCdDVE.js} +1 -1
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-9Z0QAO_7.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-BdvO8jZ2.js → tsMode-Blc1d2dp.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-BfVNzhgs.js → typescript-CV4ME9fo.js} +1 -1
- package/src/client/dist/spa/assets/{use-checkbox-D7zmRxGI.js → use-checkbox-y_fOkYZN.js} +1 -1
- package/src/client/dist/spa/assets/{use-id-CuaR1RiE.js → use-id-_7wiRcgb.js} +1 -1
- package/src/client/dist/spa/assets/{use-panel-D-8nAQns.js → use-panel-DCPiSURS.js} +1 -1
- package/src/client/dist/spa/assets/use-quasar-DQYS47mh.js +1 -0
- package/src/client/dist/spa/assets/{vue-i18n-BcfTCFFS.js → vue-i18n-DI-gS-CC.js} +1 -1
- package/src/client/dist/spa/assets/{xml-DGNXGqXL.js → xml-DLYRBBbI.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-CtAtOyt5.js → yaml-QIBjI5Dl.js} +1 -1
- package/src/client/dist/spa/index.html +12 -12
- package/src/mcp-server/kobo-tasks-server.ts +27 -0
- package/src/client/dist/spa/assets/ActivityFeed-BboSPm4b.js +0 -7
- package/src/client/dist/spa/assets/CreatePage-BDObLDJc.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +0 -1
- package/src/client/dist/spa/assets/HealthPage-CBSw7e5q.js +0 -1
- package/src/client/dist/spa/assets/QIcon-BJuyqdsT.js +0 -1
- package/src/client/dist/spa/assets/QSpace-CLtL3aPy.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-C1efO0VM.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-3jcof896.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-CCtIrBiR.css +0 -1
- package/src/client/dist/spa/assets/i18n-CLY0XI9-.js +0 -1
- package/src/client/dist/spa/assets/index-D6wj_wQ9.js +0 -2
- package/src/client/dist/spa/assets/models-BsjWUKqM.js +0 -1
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +0 -1
- 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 {};
|