@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
|
@@ -42,18 +42,16 @@ function isAgentPermissionMode(value) {
|
|
|
42
42
|
*
|
|
43
43
|
* Cascade: explicit body field → global default (validated) → 'bypass'.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* The per-engine default lives in `defaultPermissionModeByEngine[engineId]`
|
|
46
|
+
* (added in settings v20). If the engine id is missing or its entry is invalid,
|
|
47
|
+
* we fall back to 'bypass' (the safest non-plan default).
|
|
48
48
|
*/
|
|
49
|
-
function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSettings) {
|
|
49
|
+
function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSettings, engineId) {
|
|
50
50
|
if (isAgentPermissionMode(bodyValue))
|
|
51
51
|
return bodyValue;
|
|
52
|
-
const global = globalSettings.
|
|
52
|
+
const global = globalSettings.defaultPermissionModeByEngine?.[engineId];
|
|
53
53
|
if (isAgentPermissionMode(global))
|
|
54
54
|
return global;
|
|
55
|
-
if (global === 'plan')
|
|
56
|
-
return 'plan';
|
|
57
55
|
return 'bypass';
|
|
58
56
|
}
|
|
59
57
|
app.get('/', (c) => {
|
|
@@ -82,10 +80,25 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
82
80
|
// engine is rejected up-front so we don't create orphan workspaces that
|
|
83
81
|
// can't spawn an agent.
|
|
84
82
|
if (body.engine) {
|
|
85
|
-
const
|
|
83
|
+
const engines = listEngines();
|
|
84
|
+
const validEngineIds = engines.map((e) => e.id);
|
|
86
85
|
if (!validEngineIds.includes(body.engine)) {
|
|
87
86
|
return c.json({ error: `Unknown engine '${body.engine}'. Valid engines: ${validEngineIds.join(', ')}` }, 400);
|
|
88
87
|
}
|
|
88
|
+
// Cross-validate engine × permission mode: each engine declares which
|
|
89
|
+
// modes it supports via `capabilities.permissionModes`. The UI already
|
|
90
|
+
// filters, but API consumers can still send any combo. Reject up-front
|
|
91
|
+
// so we don't park workspaces in a permanently broken state (e.g. Codex
|
|
92
|
+
// workspaces with `interactive` mode hang on the first tool call).
|
|
93
|
+
if (body.agentPermissionMode) {
|
|
94
|
+
const engine = engines.find((e) => e.id === body.engine);
|
|
95
|
+
const supported = engine?.capabilities.permissionModes ?? [];
|
|
96
|
+
if (!supported.includes(body.agentPermissionMode)) {
|
|
97
|
+
return c.json({
|
|
98
|
+
error: `Engine '${body.engine}' does not support agentPermissionMode '${body.agentPermissionMode}'. Supported: ${supported.join(', ')}`,
|
|
99
|
+
}, 400);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
103
|
// Fetch the source branch from origin first — if this fails, block creation
|
|
91
104
|
// immediately (no DB records created, user stays on the create page).
|
|
@@ -222,7 +235,7 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
222
235
|
worktreeOwned: !useReusedWorktree,
|
|
223
236
|
model: body.model,
|
|
224
237
|
reasoningEffort: body.reasoningEffort,
|
|
225
|
-
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings),
|
|
238
|
+
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
|
|
226
239
|
engine: body.engine,
|
|
227
240
|
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
228
241
|
});
|
|
@@ -1451,6 +1464,17 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
1451
1464
|
if (!isAgentPermissionMode(body.agentPermissionMode)) {
|
|
1452
1465
|
return c.json({ error: `Invalid agentPermissionMode. Must be one of: ${VALID_AGENT_PERMISSION_MODES.join(', ')}` }, 400);
|
|
1453
1466
|
}
|
|
1467
|
+
// Cross-validate against the engine's declared capabilities — see the
|
|
1468
|
+
// POST route for the same guard. Prevents parking a workspace in a mode
|
|
1469
|
+
// its engine cannot honour.
|
|
1470
|
+
const engineId = workspace.engine;
|
|
1471
|
+
const engine = listEngines().find((e) => e.id === engineId);
|
|
1472
|
+
const supported = engine?.capabilities.permissionModes ?? [];
|
|
1473
|
+
if (engine && !supported.includes(body.agentPermissionMode)) {
|
|
1474
|
+
return c.json({
|
|
1475
|
+
error: `Engine '${engineId}' does not support agentPermissionMode '${body.agentPermissionMode}'. Supported: ${supported.join(', ')}`,
|
|
1476
|
+
}, 400);
|
|
1477
|
+
}
|
|
1454
1478
|
updated = workspaceService.updateAgentPermissionMode(id, body.agentPermissionMode);
|
|
1455
1479
|
}
|
|
1456
1480
|
if (body.status) {
|
|
@@ -4,14 +4,21 @@ export const CLAUDE_CODE_CAPABILITIES = {
|
|
|
4
4
|
// ONE source of truth, consumed both by this file (for /api/engines and
|
|
5
5
|
// for validation in POST /api/workspaces) and by the frontend selectors.
|
|
6
6
|
models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
7
|
+
// Reasoning effort values passed verbatim via `options.extraArgs.effort` to
|
|
8
|
+
// the Claude SDK. `auto` is a Kōbō sentinel meaning "let the SDK pick its
|
|
9
|
+
// default" (we omit the arg).
|
|
7
10
|
effortLevels: [
|
|
8
11
|
{ id: 'auto', label: 'Auto' },
|
|
9
12
|
{ id: 'low', label: 'Low' },
|
|
10
13
|
{ id: 'medium', label: 'Medium' },
|
|
11
14
|
{ id: 'high', label: 'High' },
|
|
15
|
+
{ id: 'xhigh', label: 'Extra High' },
|
|
16
|
+
{ id: 'max', label: 'Max' },
|
|
12
17
|
],
|
|
13
18
|
permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
|
|
14
19
|
supportsResume: true,
|
|
15
20
|
supportsMcp: true,
|
|
16
21
|
supportsSkills: true,
|
|
22
|
+
supportsSubagents: true,
|
|
23
|
+
supportsQuotaStatus: true,
|
|
17
24
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CODEX_MODELS } from '../../../../../shared/codex-models.js';
|
|
2
|
+
export const CODEX_CAPABILITIES = {
|
|
3
|
+
models: CODEX_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
4
|
+
effortLevels: [
|
|
5
|
+
{ id: 'auto', label: 'Auto' },
|
|
6
|
+
{ id: 'minimal', label: 'Minimal' },
|
|
7
|
+
{ id: 'low', label: 'Low' },
|
|
8
|
+
{ id: 'medium', label: 'Medium' },
|
|
9
|
+
{ id: 'high', label: 'High' },
|
|
10
|
+
{ id: 'xhigh', label: 'Extra High' },
|
|
11
|
+
],
|
|
12
|
+
permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
|
|
13
|
+
supportsResume: true,
|
|
14
|
+
supportsMcp: true,
|
|
15
|
+
supportsSkills: false,
|
|
16
|
+
supportsSubagents: true,
|
|
17
|
+
supportsQuotaStatus: true,
|
|
18
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createJsonRpcPeer } from './jsonrpc/peer.js';
|
|
2
|
+
export function createAppServerClient(opts) {
|
|
3
|
+
const peer = createJsonRpcPeer({
|
|
4
|
+
stdin: opts.stdin,
|
|
5
|
+
stdout: opts.stdout,
|
|
6
|
+
onNotification: opts.onNotification ?? (() => { }),
|
|
7
|
+
onServerRequest: opts.onServerRequest ?? (() => { }),
|
|
8
|
+
onError: opts.onError,
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
peer,
|
|
12
|
+
async connect() {
|
|
13
|
+
// Without experimentalApi the server rejects collaborationMode (-32600).
|
|
14
|
+
const params = {
|
|
15
|
+
clientInfo: opts.clientInfo,
|
|
16
|
+
capabilities: { experimentalApi: true },
|
|
17
|
+
};
|
|
18
|
+
return peer.request('initialize', params);
|
|
19
|
+
},
|
|
20
|
+
startThread(params) {
|
|
21
|
+
return peer.request('thread/start', params);
|
|
22
|
+
},
|
|
23
|
+
resumeThread(params) {
|
|
24
|
+
return peer.request('thread/resume', params);
|
|
25
|
+
},
|
|
26
|
+
startTurn(params) {
|
|
27
|
+
return peer.request('turn/start', params);
|
|
28
|
+
},
|
|
29
|
+
async interruptTurn(params) {
|
|
30
|
+
await peer.request('turn/interrupt', params);
|
|
31
|
+
},
|
|
32
|
+
close() {
|
|
33
|
+
peer.close();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { getPackageVersion } from '../../../../utils/paths.js';
|
|
2
|
+
import { CODEX_CAPABILITIES } from './capabilities.js';
|
|
3
|
+
import { createAppServerClient } from './client.js';
|
|
4
|
+
import { createMapperState, emitSessionStarted, handleAgentMessageDelta, handleItemCompleted, handleItemStarted, handleRateLimitsUpdated, handleTurnCompleted, QUOTA_PATTERN, tryEmitQuota, } from './event-mapper.js';
|
|
5
|
+
import { buildCodexOptions } from './options-builder.js';
|
|
6
|
+
import { buildResponseForResolve, handleServerRequest } from './server-requests.js';
|
|
7
|
+
import { spawnAppServer } from './spawn.js';
|
|
8
|
+
/**
|
|
9
|
+
* Heuristic for detecting a stale/expired thread id on `thread/resume`.
|
|
10
|
+
* Canonical wording isn't captured yet — when matched, the engine emits
|
|
11
|
+
* `error/resume_failed` so the orchestrator can restart with a fresh thread.
|
|
12
|
+
*/
|
|
13
|
+
export const RESUME_FAILED_PATTERN = /(thread\b.*\bnot found|session\b.*\bnot found|no\s+(such\s+)?thread|thread.*expired|conversation\b.*\bnot found|invalid\s+thread\s+id)/i;
|
|
14
|
+
export function createCodexEngine() {
|
|
15
|
+
return {
|
|
16
|
+
id: 'codex',
|
|
17
|
+
displayName: 'OpenAI Codex',
|
|
18
|
+
capabilities: CODEX_CAPABILITIES,
|
|
19
|
+
async start(options, onEvent) {
|
|
20
|
+
const { threadParams, input, isResume, collaborationMode } = buildCodexOptions({
|
|
21
|
+
prompt: options.prompt,
|
|
22
|
+
model: options.model,
|
|
23
|
+
effort: options.effort,
|
|
24
|
+
agentPermissionMode: options.agentPermissionMode ?? 'bypass',
|
|
25
|
+
resumeFromEngineSessionId: options.resumeFromEngineSessionId,
|
|
26
|
+
workingDir: options.workingDir,
|
|
27
|
+
mcpServers: options.mcpServers,
|
|
28
|
+
});
|
|
29
|
+
const mapperState = createMapperState();
|
|
30
|
+
const abortController = new AbortController();
|
|
31
|
+
const pendingByCallId = new Map();
|
|
32
|
+
let iteratorRunning = false;
|
|
33
|
+
let userInterrupted = false;
|
|
34
|
+
let discoveredSessionId = options.resumeFromEngineSessionId;
|
|
35
|
+
const safeEmit = (ev) => {
|
|
36
|
+
try {
|
|
37
|
+
onEvent(ev);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error('[codex-engine] onEvent handler threw:', err);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
|
|
44
|
+
if (child.stderr) {
|
|
45
|
+
child.stderr.setEncoding('utf8');
|
|
46
|
+
child.stderr.on('data', (chunk) => {
|
|
47
|
+
const text = chunk.toString();
|
|
48
|
+
if (QUOTA_PATTERN.test(text)) {
|
|
49
|
+
tryEmitQuota(mapperState, safeEmit, text.trim());
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.warn('[codex] stderr:', text.trimEnd());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
let resolveTurnDone;
|
|
57
|
+
let rejectTurnDone;
|
|
58
|
+
const turnDonePromise = new Promise((resolve, reject) => {
|
|
59
|
+
resolveTurnDone = resolve;
|
|
60
|
+
rejectTurnDone = reject;
|
|
61
|
+
});
|
|
62
|
+
abortController.signal.addEventListener('abort', () => {
|
|
63
|
+
const err = new Error('AbortError');
|
|
64
|
+
err.name = 'AbortError';
|
|
65
|
+
rejectTurnDone(err);
|
|
66
|
+
});
|
|
67
|
+
const client = createAppServerClient({
|
|
68
|
+
stdin: child.stdin,
|
|
69
|
+
stdout: child.stdout,
|
|
70
|
+
clientInfo: { name: 'kobo', version: getPackageVersion() },
|
|
71
|
+
onNotification(method, params) {
|
|
72
|
+
// Ignored notifications — harmless bookkeeping by the server
|
|
73
|
+
if (method === 'mcpServer/startupStatus/updated' ||
|
|
74
|
+
method === 'thread/started' ||
|
|
75
|
+
method === 'thread/status/changed' ||
|
|
76
|
+
method === 'remoteControl/status/changed' ||
|
|
77
|
+
method === 'turn/started') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (method === 'item/started') {
|
|
81
|
+
const n = params;
|
|
82
|
+
for (const ev of handleItemStarted(n.item, mapperState))
|
|
83
|
+
safeEmit(ev);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (method === 'item/completed') {
|
|
87
|
+
const n = params;
|
|
88
|
+
for (const ev of handleItemCompleted(n.item, mapperState))
|
|
89
|
+
safeEmit(ev);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (method === 'item/agentMessage/delta') {
|
|
93
|
+
const n = params;
|
|
94
|
+
for (const ev of handleAgentMessageDelta(n, mapperState))
|
|
95
|
+
safeEmit(ev);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (method === 'turn/completed') {
|
|
99
|
+
const n = params;
|
|
100
|
+
for (const ev of handleTurnCompleted(n, mapperState))
|
|
101
|
+
safeEmit(ev);
|
|
102
|
+
resolveTurnDone();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
106
|
+
const p = params;
|
|
107
|
+
if (p?.tokenUsage?.last) {
|
|
108
|
+
const last = p.tokenUsage.last;
|
|
109
|
+
safeEmit({
|
|
110
|
+
kind: 'usage',
|
|
111
|
+
inputTokens: last.inputTokens,
|
|
112
|
+
outputTokens: last.outputTokens + last.reasoningOutputTokens,
|
|
113
|
+
cacheRead: last.cachedInputTokens,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (method === 'account/rateLimits/updated') {
|
|
119
|
+
for (const ev of handleRateLimitsUpdated(params, mapperState))
|
|
120
|
+
safeEmit(ev);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (method === 'error') {
|
|
124
|
+
const n = params;
|
|
125
|
+
const msg = n?.message ?? 'unknown error';
|
|
126
|
+
if (QUOTA_PATTERN.test(msg)) {
|
|
127
|
+
tryEmitQuota(mapperState, safeEmit, msg);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
mapperState.sawErrorResult = true;
|
|
131
|
+
safeEmit({ kind: 'error', category: 'other', message: msg });
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
onServerRequest(id, method, params) {
|
|
137
|
+
handleServerRequest({
|
|
138
|
+
requestId: id,
|
|
139
|
+
method,
|
|
140
|
+
params,
|
|
141
|
+
emit: safeEmit,
|
|
142
|
+
register(callId, pending) {
|
|
143
|
+
pendingByCallId.set(callId, pending);
|
|
144
|
+
},
|
|
145
|
+
respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
onError(err) {
|
|
149
|
+
console.error('[codex] JSON-RPC transport error:', err);
|
|
150
|
+
rejectTurnDone(err);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const iteratorPromise = (async () => {
|
|
154
|
+
iteratorRunning = true;
|
|
155
|
+
try {
|
|
156
|
+
await client.connect();
|
|
157
|
+
if (isResume && options.resumeFromEngineSessionId) {
|
|
158
|
+
await client.resumeThread({
|
|
159
|
+
threadId: options.resumeFromEngineSessionId,
|
|
160
|
+
cwd: options.workingDir,
|
|
161
|
+
persistExtendedHistory: false,
|
|
162
|
+
...(threadParams.model != null ? { model: threadParams.model } : {}),
|
|
163
|
+
...(threadParams.approvalPolicy != null ? { approvalPolicy: threadParams.approvalPolicy } : {}),
|
|
164
|
+
...(threadParams.sandbox != null ? { sandbox: threadParams.sandbox } : {}),
|
|
165
|
+
...(threadParams.modelReasoningEffort != null
|
|
166
|
+
? { modelReasoningEffort: threadParams.modelReasoningEffort }
|
|
167
|
+
: {}),
|
|
168
|
+
...(threadParams.config != null ? { config: threadParams.config } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const startResp = await client.startThread(threadParams);
|
|
173
|
+
discoveredSessionId = startResp.thread.id;
|
|
174
|
+
}
|
|
175
|
+
for (const ev of emitSessionStarted(discoveredSessionId, mapperState))
|
|
176
|
+
safeEmit(ev);
|
|
177
|
+
// collaborationMode is sticky server-side — always send it explicitly,
|
|
178
|
+
// never omit (would leave a Bypass turn stuck in a previous Plan mode).
|
|
179
|
+
await client.startTurn({
|
|
180
|
+
threadId: discoveredSessionId,
|
|
181
|
+
input,
|
|
182
|
+
collaborationMode,
|
|
183
|
+
});
|
|
184
|
+
await turnDonePromise;
|
|
185
|
+
const reason = mapperState.sawErrorResult
|
|
186
|
+
? 'error'
|
|
187
|
+
: mapperState.sawTurnInterrupted
|
|
188
|
+
? 'killed'
|
|
189
|
+
: 'completed';
|
|
190
|
+
safeEmit({
|
|
191
|
+
kind: 'session:ended',
|
|
192
|
+
reason,
|
|
193
|
+
exitCode: reason === 'completed' ? 0 : null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
const error = err;
|
|
198
|
+
const message = error.message ?? String(err);
|
|
199
|
+
const isAbort = userInterrupted || error.name === 'AbortError' || abortController.signal.aborted;
|
|
200
|
+
const isResumeAttempt = options.resumeFromEngineSessionId !== undefined;
|
|
201
|
+
if (isAbort) {
|
|
202
|
+
safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
|
|
203
|
+
}
|
|
204
|
+
else if (QUOTA_PATTERN.test(message)) {
|
|
205
|
+
tryEmitQuota(mapperState, safeEmit, message);
|
|
206
|
+
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
207
|
+
}
|
|
208
|
+
else if (isResumeAttempt && RESUME_FAILED_PATTERN.test(message)) {
|
|
209
|
+
safeEmit({ kind: 'error', category: 'resume_failed', message });
|
|
210
|
+
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
safeEmit({ kind: 'error', category: 'spawn_failed', message });
|
|
214
|
+
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
iteratorRunning = false;
|
|
219
|
+
client.close();
|
|
220
|
+
try {
|
|
221
|
+
child.kill('SIGTERM');
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// best-effort
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
})();
|
|
228
|
+
const engineProcess = {
|
|
229
|
+
get pid() {
|
|
230
|
+
return child.pid;
|
|
231
|
+
},
|
|
232
|
+
get engineSessionId() {
|
|
233
|
+
return discoveredSessionId;
|
|
234
|
+
},
|
|
235
|
+
isAlive() {
|
|
236
|
+
return iteratorRunning;
|
|
237
|
+
},
|
|
238
|
+
sendMessage() {
|
|
239
|
+
throw new Error('sendMessage not supported in Codex app-server single-shot mode');
|
|
240
|
+
},
|
|
241
|
+
interrupt() {
|
|
242
|
+
userInterrupted = true;
|
|
243
|
+
abortController.abort();
|
|
244
|
+
if (discoveredSessionId) {
|
|
245
|
+
client.interruptTurn({ threadId: discoveredSessionId }).catch(() => { });
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async stop() {
|
|
249
|
+
abortController.abort();
|
|
250
|
+
try {
|
|
251
|
+
await iteratorPromise;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// swallow — best effort
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
child.stdin?.end();
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// swallow
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
resolvePendingUserInput(callId, response) {
|
|
264
|
+
const pending = pendingByCallId.get(callId);
|
|
265
|
+
if (!pending)
|
|
266
|
+
return false;
|
|
267
|
+
pendingByCallId.delete(callId);
|
|
268
|
+
const result = buildResponseForResolve(pending, response);
|
|
269
|
+
client.peer.respond(pending.requestId, result);
|
|
270
|
+
return true;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
return engineProcess;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|