@loicngr/kobo 1.11.39 → 1.11.40
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 +12 -0
- package/CHANGELOG.md +4 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +225 -241
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-MqsC-95i.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BxURWYxa.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-DjiMDAUi.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-mA5RZf1t.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-Ci7ucmy3.js +8 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-RZTvcJkT.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-CTOc3yA0.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-D3__hKJ5.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-td0RAVPx.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-BkyFPV7a.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-WspK3KC7.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-7pw7iuV0.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CLNM13oo.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CmRgIvu4.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-CCHGEsKf.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-ijmsYk-2.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DuQFjlQF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-C5RF0lOK.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BR_XElog.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BAC6u9OP.css → WorkspacePage-CqcMweNL.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-DUqzdkbg.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-ImiSpzfI.js} +1 -1
- package/src/client/dist/pwa/assets/de-CaHID9mW.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-2C1hn1pw.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-Bm1vVaBr.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-_D8DFYXo.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-CZ9qfBYq.js +1 -0
- package/src/client/dist/pwa/assets/es-ZhzlCmer.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-B6xAlAaM.js → expand-template-CbDNnTPP.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-pPImdFUy.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-dTX38ZsV.js} +1 -1
- package/src/client/dist/pwa/assets/fr-BfXw7L7J.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-DGluDnAm.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-BgviTnpD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-CV-MrunL.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +1 -0
- package/src/client/dist/pwa/assets/index-9tiw3NjK.js +20 -0
- package/src/client/dist/pwa/assets/it-CHAurcTF.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C2VFcHf2.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-Dg9ZhbP5.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-ImgrPYNl.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-waWZXS1T.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-BB2rtCHM.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-h6O0qCMA.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-Dyg-FGI6.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-Dfpg92li.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-DDWmNBW1.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-XUfY_zbw.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DVLs-DBA.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-D34BqMwB.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-DwUkPy-O.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-C7-UpNXx.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-DfE-kwrX.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-DwwdOEAq.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-Cx1oQhEr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-BQOiJsdx.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +118 -2
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
|
@@ -142,6 +142,7 @@ export function createClaudeCodeEngine() {
|
|
|
142
142
|
// A user decision is intentional inactivity — suspend the deadline
|
|
143
143
|
// rather than cancel it, so it resumes the moment the card is answered.
|
|
144
144
|
turnLiveness.pause();
|
|
145
|
+
clearSubagentStallWatchdog();
|
|
145
146
|
const onAbort = () => {
|
|
146
147
|
if (pendingResolvers.get(toolCallId) === resolver) {
|
|
147
148
|
pendingResolvers.delete(toolCallId);
|
|
@@ -327,6 +328,10 @@ export function createClaudeCodeEngine() {
|
|
|
327
328
|
// windows. Every call site that changes one of these three conditions
|
|
328
329
|
// must re-evaluate through this helper, not duplicate the check.
|
|
329
330
|
const reevaluateLivenessPause = () => {
|
|
331
|
+
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
332
|
+
clearSubagentStallWatchdog();
|
|
333
|
+
else if (!subagentStallTimer)
|
|
334
|
+
armSubagentStallWatchdog();
|
|
330
335
|
if (activeSubagentTaskIds.size > 0 || pendingResolvers.size > 0 || isCompacting)
|
|
331
336
|
turnLiveness.pause();
|
|
332
337
|
else
|
|
@@ -355,16 +360,6 @@ export function createClaudeCodeEngine() {
|
|
|
355
360
|
isCompacting = false;
|
|
356
361
|
safeEmit({ kind: 'session:compacting', active: false });
|
|
357
362
|
reevaluateLivenessPause();
|
|
358
|
-
if (activeSubagentTaskIds.size > 0 && !subagentStallTimer) {
|
|
359
|
-
// Resume declined because a subagent is still tracked — but the
|
|
360
|
-
// subagent stall net is only armed from the `result` branch, which
|
|
361
|
-
// a fully wedged generator may never reach. Arm it here as a
|
|
362
|
-
// second stage so this combination can't hang forever. The
|
|
363
|
-
// `!subagentStallTimer` guard matters: arming re-sets the
|
|
364
|
-
// deadline, and a net already armed by a prior `result` must not
|
|
365
|
-
// be extended by this backstop (it is not new activity).
|
|
366
|
-
armSubagentStallWatchdog();
|
|
367
|
-
}
|
|
368
363
|
}, COMPACTION_STALL_TIMEOUT_MS);
|
|
369
364
|
compactionStallTimer.unref?.();
|
|
370
365
|
};
|
|
@@ -406,27 +401,24 @@ export function createClaudeCodeEngine() {
|
|
|
406
401
|
clearTimeout(subagentStallTimer);
|
|
407
402
|
subagentStallTimer = undefined;
|
|
408
403
|
};
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// message below — a long chain of legitimate subagent-backed turns
|
|
412
|
-
// must not be cut off just because 10 minutes have passed in total.
|
|
404
|
+
// Runs from the first task_started, including before the first result.
|
|
405
|
+
// Human waits suspend it independently; genuine activity restarts it.
|
|
413
406
|
const armSubagentStallWatchdog = () => {
|
|
414
407
|
clearSubagentStallWatchdog();
|
|
408
|
+
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
409
|
+
return;
|
|
415
410
|
subagentStallTimer = setTimeout(() => {
|
|
416
411
|
subagentStallTimer = undefined;
|
|
417
|
-
|
|
418
|
-
// about to start on this same stream — don't discard the subagent
|
|
419
|
-
// tracking or force-close under it; the next 'result' re-evaluates.
|
|
420
|
-
if (inputStream.hasUnansweredInput(completedResponses))
|
|
412
|
+
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
421
413
|
return;
|
|
422
|
-
console.warn(`[claude-engine]
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
414
|
+
console.warn(`[claude-engine] No subagent activity for ${SUBAGENT_STALL_TIMEOUT_MS}ms — forcing session:ended`);
|
|
415
|
+
safeEmit({
|
|
416
|
+
kind: 'error',
|
|
417
|
+
category: 'other',
|
|
418
|
+
message: 'Session force-ended: background subagents stopped reporting activity (watchdog).',
|
|
419
|
+
});
|
|
420
|
+
emitSessionEnded('watchdog', null);
|
|
421
|
+
abortController.abort();
|
|
430
422
|
}, SUBAGENT_STALL_TIMEOUT_MS);
|
|
431
423
|
subagentStallTimer.unref?.();
|
|
432
424
|
};
|
|
@@ -454,6 +446,14 @@ export function createClaudeCodeEngine() {
|
|
|
454
446
|
else
|
|
455
447
|
activeSubagentTaskIds.delete(ev.toolCallId);
|
|
456
448
|
}
|
|
449
|
+
if (activeSubagentTaskIds.size > 0 &&
|
|
450
|
+
(events.some((ev) => ev.kind === 'subagent:progress' ||
|
|
451
|
+
ev.kind === 'message:text' ||
|
|
452
|
+
ev.kind === 'message:thinking' ||
|
|
453
|
+
ev.kind === 'tool:call' ||
|
|
454
|
+
ev.kind === 'tool:result') ||
|
|
455
|
+
('parent_tool_use_id' in msg && msg.parent_tool_use_id != null)))
|
|
456
|
+
armSubagentStallWatchdog();
|
|
457
457
|
for (const ev of events) {
|
|
458
458
|
if (ev.kind === 'session:compacting') {
|
|
459
459
|
isCompacting = ev.active;
|
|
@@ -485,10 +485,8 @@ export function createClaudeCodeEngine() {
|
|
|
485
485
|
else if (ev.kind === 'tool:result')
|
|
486
486
|
pendingToolCallIds.delete(ev.toolCallId);
|
|
487
487
|
}
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
// reports done). React the moment the last one clears instead of
|
|
491
|
-
// waiting for a 'result' message that may never come.
|
|
488
|
+
// After a settled result, the last background completion can
|
|
489
|
+
// drain the stream without waiting for another result.
|
|
492
490
|
if (subagentStallTimer &&
|
|
493
491
|
activeSubagentTaskIds.size === 0 &&
|
|
494
492
|
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
@@ -601,6 +599,7 @@ export function createClaudeCodeEngine() {
|
|
|
601
599
|
if (!iteratorRunning)
|
|
602
600
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
603
601
|
inputStream.send(text);
|
|
602
|
+
armSubagentStallWatchdog();
|
|
604
603
|
},
|
|
605
604
|
sendWakeupIfWaiting(text) {
|
|
606
605
|
if (!iteratorRunning ||
|
|
@@ -615,9 +614,8 @@ export function createClaudeCodeEngine() {
|
|
|
615
614
|
return false;
|
|
616
615
|
inputStream.send(text);
|
|
617
616
|
waitingForBackground = false;
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
clearSubagentStallWatchdog();
|
|
617
|
+
// Sending a new turn restarts, but never removes, the stall bound.
|
|
618
|
+
armSubagentStallWatchdog();
|
|
621
619
|
return true;
|
|
622
620
|
},
|
|
623
621
|
interrupt() {
|
|
@@ -220,6 +220,8 @@ export function createCodexEngine() {
|
|
|
220
220
|
err.name = 'AbortError';
|
|
221
221
|
rejectTurnDone(err);
|
|
222
222
|
});
|
|
223
|
+
const fileChanges = new Map();
|
|
224
|
+
const itemKey = (threadId, turnId, itemId) => JSON.stringify([threadId, turnId, itemId]);
|
|
223
225
|
const client = createAppServerClient({
|
|
224
226
|
stdin: child.stdin,
|
|
225
227
|
stdout: child.stdout,
|
|
@@ -253,6 +255,8 @@ export function createCodexEngine() {
|
|
|
253
255
|
}
|
|
254
256
|
if (method === 'item/started') {
|
|
255
257
|
const n = params;
|
|
258
|
+
if (n.item.type === 'fileChange')
|
|
259
|
+
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
256
260
|
const events = handleItemStarted(n.item, mapperState);
|
|
257
261
|
for (const ev of events)
|
|
258
262
|
safeEmit(ev);
|
|
@@ -272,6 +276,8 @@ export function createCodexEngine() {
|
|
|
272
276
|
}
|
|
273
277
|
if (method === 'item/completed') {
|
|
274
278
|
const n = params;
|
|
279
|
+
if (n.item.type === 'fileChange')
|
|
280
|
+
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
275
281
|
const events = handleItemCompleted(n.item, mapperState);
|
|
276
282
|
for (const ev of events)
|
|
277
283
|
safeEmit(ev);
|
|
@@ -302,6 +308,11 @@ export function createCodexEngine() {
|
|
|
302
308
|
}
|
|
303
309
|
if (method === 'turn/completed') {
|
|
304
310
|
const n = params;
|
|
311
|
+
for (const key of fileChanges.keys()) {
|
|
312
|
+
const [threadId, turnId] = JSON.parse(key);
|
|
313
|
+
if (threadId === n.threadId && turnId === n.turn.id)
|
|
314
|
+
fileChanges.delete(key);
|
|
315
|
+
}
|
|
305
316
|
for (const ev of handleTurnCompleted(n, mapperState))
|
|
306
317
|
safeEmit(ev);
|
|
307
318
|
if (!activeTurnId || !n.turn?.id || n.turn.id === activeTurnId) {
|
|
@@ -338,7 +349,9 @@ export function createCodexEngine() {
|
|
|
338
349
|
}
|
|
339
350
|
if (method === 'error') {
|
|
340
351
|
const n = params;
|
|
341
|
-
|
|
352
|
+
if (n.willRetry)
|
|
353
|
+
return;
|
|
354
|
+
const msg = n?.error?.message ?? 'unknown error';
|
|
342
355
|
if (QUOTA_PATTERN.test(msg)) {
|
|
343
356
|
tryEmitQuota(mapperState, safeEmit, msg);
|
|
344
357
|
}
|
|
@@ -361,6 +374,14 @@ export function createCodexEngine() {
|
|
|
361
374
|
},
|
|
362
375
|
respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
|
|
363
376
|
respond: (reqId, result) => client.peer.respond(reqId, result),
|
|
377
|
+
resolveFileChange: (params) => {
|
|
378
|
+
if (typeof params.threadId !== 'string' ||
|
|
379
|
+
typeof params.turnId !== 'string' ||
|
|
380
|
+
typeof params.itemId !== 'string')
|
|
381
|
+
return undefined;
|
|
382
|
+
const item = fileChanges.get(itemKey(params.threadId, params.turnId, params.itemId));
|
|
383
|
+
return item ? { changes: item.changes, cwd: options.workingDir } : undefined;
|
|
384
|
+
},
|
|
364
385
|
autoApprove: (toolName, payload) => isWorkspacePermissionAllowed(options.workspaceId, { engine: 'codex', toolName, payload }),
|
|
365
386
|
});
|
|
366
387
|
},
|
|
@@ -379,8 +400,9 @@ export function createCodexEngine() {
|
|
|
379
400
|
// install hangs.
|
|
380
401
|
turnLiveness.start();
|
|
381
402
|
await waitForChild(client.connect());
|
|
403
|
+
let threadResponse;
|
|
382
404
|
if (isResume && options.resumeFromEngineSessionId) {
|
|
383
|
-
await waitForChild(client.resumeThread({
|
|
405
|
+
threadResponse = await waitForChild(client.resumeThread({
|
|
384
406
|
threadId: options.resumeFromEngineSessionId,
|
|
385
407
|
cwd: options.workingDir,
|
|
386
408
|
persistExtendedHistory: false,
|
|
@@ -394,8 +416,14 @@ export function createCodexEngine() {
|
|
|
394
416
|
}));
|
|
395
417
|
}
|
|
396
418
|
else {
|
|
397
|
-
|
|
398
|
-
discoveredSessionId =
|
|
419
|
+
threadResponse = await waitForChild(client.startThread(threadParams));
|
|
420
|
+
discoveredSessionId = threadResponse.thread.id;
|
|
421
|
+
}
|
|
422
|
+
// "auto" is a Kōbō sentinel, never a provider model identifier. The
|
|
423
|
+
// collaboration override must use the model the app-server resolved.
|
|
424
|
+
const model = threadParams.model ?? threadResponse.model;
|
|
425
|
+
if (typeof model !== 'string' || !model.trim() || model === 'auto') {
|
|
426
|
+
throw new Error('Codex did not return a resolved model. Select an explicit model or update the Codex CLI.');
|
|
399
427
|
}
|
|
400
428
|
for (const ev of emitSessionStarted(discoveredSessionId, mapperState))
|
|
401
429
|
safeEmit(ev);
|
|
@@ -404,9 +432,9 @@ export function createCodexEngine() {
|
|
|
404
432
|
const initialTurn = await waitForChild(client.startTurn({
|
|
405
433
|
threadId: discoveredSessionId,
|
|
406
434
|
input,
|
|
407
|
-
collaborationMode,
|
|
435
|
+
collaborationMode: { ...collaborationMode, settings: { ...collaborationMode.settings, model } },
|
|
408
436
|
}));
|
|
409
|
-
activeTurnId = initialTurn.
|
|
437
|
+
activeTurnId = initialTurn.turn.id;
|
|
410
438
|
turnLiveness.activity();
|
|
411
439
|
readySettled = true;
|
|
412
440
|
resolveReady();
|
|
@@ -468,6 +496,7 @@ export function createCodexEngine() {
|
|
|
468
496
|
// best-effort
|
|
469
497
|
}
|
|
470
498
|
}
|
|
499
|
+
fileChanges.clear();
|
|
471
500
|
pendingByCallId.clear();
|
|
472
501
|
pendingToolCalls.clear();
|
|
473
502
|
client.close();
|
|
@@ -526,9 +555,9 @@ export function createCodexEngine() {
|
|
|
526
555
|
if (gracefulInterruptPromise)
|
|
527
556
|
return;
|
|
528
557
|
gracefulInterruptPromise = (async () => {
|
|
529
|
-
if (discoveredSessionId) {
|
|
558
|
+
if (discoveredSessionId && activeTurnId) {
|
|
530
559
|
await Promise.race([
|
|
531
|
-
client.interruptTurn({ threadId: discoveredSessionId }).catch(() => { }),
|
|
560
|
+
client.interruptTurn({ threadId: discoveredSessionId, turnId: activeTurnId }).catch(() => { }),
|
|
532
561
|
wait(CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS),
|
|
533
562
|
]);
|
|
534
563
|
}
|
|
@@ -8,6 +8,8 @@ import { nanoid } from 'nanoid';
|
|
|
8
8
|
// - "out of extra usage" / "out of usage"
|
|
9
9
|
// - "usage limit exceeded"
|
|
10
10
|
export const QUOTA_PATTERN = /\b429\b|rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
|
|
11
|
+
// Standalone provider notices only: normal assistant prose can discuss quotas.
|
|
12
|
+
const ASSISTANT_QUOTA_NOTICE = /^(?:rate[_ ]limit (?:exceeded|reached)|quota exceeded|insufficient[_ ]quota|out of (?:extra )?usage|usage limit (?:exceeded|reached)|you(?:'ve| have) (?:hit|reached) (?:your |the )?usage limit)(?:[.!]?(?:[ \t]*[·—-]?[ \t]*resets?\b[^\r\n]*)?)?[.!]?$/i;
|
|
11
13
|
export function createMapperState(opts) {
|
|
12
14
|
return {
|
|
13
15
|
sessionStartedEmitted: false,
|
|
@@ -204,7 +206,7 @@ export function handleItemCompleted(item, state) {
|
|
|
204
206
|
if (text.includes('[BRAINSTORM_COMPLETE]')) {
|
|
205
207
|
events.push({ kind: 'session:brainstorm-complete' });
|
|
206
208
|
}
|
|
207
|
-
if (
|
|
209
|
+
if (ASSISTANT_QUOTA_NOTICE.test(text.trim())) {
|
|
208
210
|
tryEmitQuotaInline(state, events, text);
|
|
209
211
|
}
|
|
210
212
|
events.push({ kind: 'message:end', messageId });
|
|
@@ -66,18 +66,19 @@ export function buildCodexOptions(input) {
|
|
|
66
66
|
// Always emit collaborationMode — it's sticky server-side, so omitting it
|
|
67
67
|
// would leave a resumed thread stuck in the previous turn's mode. Plan also
|
|
68
68
|
// gates the `request_user_input` internal tool. Settings echo the resolved
|
|
69
|
-
// model/effort because collaborationMode takes precedence over them.
|
|
69
|
+
// model/effort because collaborationMode takes precedence over them. Automatic
|
|
70
|
+
// model selection is filled from thread/start or thread/resume by the engine.
|
|
70
71
|
const collaborationMode = {
|
|
71
72
|
mode: input.agentPermissionMode === 'plan' ? 'plan' : 'default',
|
|
72
73
|
settings: {
|
|
73
|
-
model: threadParams.model
|
|
74
|
+
...(threadParams.model ? { model: threadParams.model } : {}),
|
|
74
75
|
reasoning_effort: threadParams.modelReasoningEffort ?? null,
|
|
75
76
|
developer_instructions: null,
|
|
76
77
|
},
|
|
77
78
|
};
|
|
78
79
|
return {
|
|
79
80
|
threadParams,
|
|
80
|
-
input: [{ type: 'text', text: effectivePrompt }],
|
|
81
|
+
input: [{ type: 'text', text: effectivePrompt, text_elements: [] }],
|
|
81
82
|
isResume,
|
|
82
83
|
collaborationMode,
|
|
83
84
|
};
|
|
@@ -29,7 +29,14 @@ export function handleServerRequest(args) {
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
if (method === 'item/fileChange/requestApproval' || method === 'applyPatchApproval') {
|
|
32
|
-
const
|
|
32
|
+
const details = args.resolveFileChange?.(p);
|
|
33
|
+
const payload = {
|
|
34
|
+
changes: details?.changes ?? p.changes,
|
|
35
|
+
cwd: details?.cwd,
|
|
36
|
+
grantRoot: p.grantRoot ?? null,
|
|
37
|
+
reason: p.reason,
|
|
38
|
+
operationApprovalAvailable: Boolean(details && details.changes.length > 0),
|
|
39
|
+
};
|
|
33
40
|
if (autoApprove?.('Edit', payload)) {
|
|
34
41
|
respond?.(requestId, { decision: 'accept' });
|
|
35
42
|
return true;
|
|
@@ -704,6 +704,21 @@ export function forgetTasksDoneSnapshot(workspaceId) {
|
|
|
704
704
|
export function forgetResumeFailed(workspaceId) {
|
|
705
705
|
resumeFailedSessions.delete(workspaceId);
|
|
706
706
|
}
|
|
707
|
+
/** Public reconnect snapshot: select DTO fields and detach nested JSON inputs. */
|
|
708
|
+
export function getPendingInputs(workspaceId) {
|
|
709
|
+
return (pendingQueue.get(workspaceId) ?? []).map((item) => {
|
|
710
|
+
const identity = {
|
|
711
|
+
agentSessionId: item.agentSessionId,
|
|
712
|
+
toolCallId: item.toolCallId,
|
|
713
|
+
toolName: item.toolName,
|
|
714
|
+
};
|
|
715
|
+
// Engine tool inputs are JSON. Serialization also strips accidental callbacks.
|
|
716
|
+
const input = JSON.parse(JSON.stringify((item.kind === 'question' ? item.input : item.toolInput) ?? null));
|
|
717
|
+
return item.kind === 'question'
|
|
718
|
+
? { kind: 'question', ...identity, input }
|
|
719
|
+
: { kind: 'permission', ...identity, toolInput: input };
|
|
720
|
+
});
|
|
721
|
+
}
|
|
707
722
|
/** Drop the pending question/permission queue for a workspace (called on delete). */
|
|
708
723
|
export function forgetPendingQueue(workspaceId) {
|
|
709
724
|
pendingQueue.delete(workspaceId);
|
|
@@ -1134,7 +1149,7 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1134
1149
|
* after `engine.start` resolves — callers should subscribe to WS events or
|
|
1135
1150
|
* query the controller via `_getControllers()` for tests.
|
|
1136
1151
|
*/
|
|
1137
|
-
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort) {
|
|
1152
|
+
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort, beforeDispatch) {
|
|
1138
1153
|
if (shuttingDown)
|
|
1139
1154
|
throw new Error('Cannot start an agent while the server is shutting down');
|
|
1140
1155
|
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
@@ -1231,6 +1246,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1231
1246
|
mcpServers: buildMcpServers(workspaceId),
|
|
1232
1247
|
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
1233
1248
|
};
|
|
1249
|
+
beforeDispatch?.();
|
|
1234
1250
|
let controller;
|
|
1235
1251
|
controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, controller, ev));
|
|
1236
1252
|
registerSessionLifecycleOwner(workspaceId, agentSessionId, controller);
|
|
@@ -1490,7 +1506,7 @@ export function sendWakeupIfWaiting(workspaceId, content, expectedSessionId) {
|
|
|
1490
1506
|
return true;
|
|
1491
1507
|
}
|
|
1492
1508
|
/** Write a user message to the running agent. */
|
|
1493
|
-
export async function sendMessage(workspaceId, content, expectedSessionId) {
|
|
1509
|
+
export async function sendMessage(workspaceId, content, expectedSessionId, beforeDispatch) {
|
|
1494
1510
|
assertWorkspaceNotCompacting(workspaceId);
|
|
1495
1511
|
const ctrl = controllers.get(workspaceId);
|
|
1496
1512
|
if (!ctrl) {
|
|
@@ -1500,6 +1516,7 @@ export async function sendMessage(workspaceId, content, expectedSessionId) {
|
|
|
1500
1516
|
throw new Error(`Session '${expectedSessionId}' is not active for workspace '${workspaceId}'`);
|
|
1501
1517
|
}
|
|
1502
1518
|
wakeupService.cancel(workspaceId, 'user-message');
|
|
1519
|
+
beforeDispatch?.();
|
|
1503
1520
|
await ctrl.sendMessage(content);
|
|
1504
1521
|
}
|
|
1505
1522
|
export async function sendMessageForFallback(workspaceId, content) {
|
|
@@ -1671,7 +1688,7 @@ export async function answerPendingQuestion(workspaceId, answers, expectedToolCa
|
|
|
1671
1688
|
try {
|
|
1672
1689
|
const formatted = formatDeferredAnswerForChat(questions, answers);
|
|
1673
1690
|
if (formatted) {
|
|
1674
|
-
emit(workspaceId, 'user:message', { content: formatted, sender: 'user' }, head.agentSessionId);
|
|
1691
|
+
emit(workspaceId, 'user:message', { content: formatted, sender: 'user', ...(opts?.source ? { source: opts.source } : {}) }, head.agentSessionId);
|
|
1675
1692
|
}
|
|
1676
1693
|
}
|
|
1677
1694
|
catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
2
2
|
// src/server/services/change-source-branch-service.ts
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
3
|
import * as path from 'node:path';
|
|
4
|
+
import { runBoundedProcess } from '../utils/bounded-process.js';
|
|
5
5
|
import * as gitOps from '../utils/git-ops.js';
|
|
6
6
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
7
7
|
import { getAgentStatus } from './agent/orchestrator.js';
|
|
@@ -255,9 +255,10 @@ async function runCustomScript(workspace, oldBase, newBase, script) {
|
|
|
255
255
|
return { status: 'dirty', forcePushNeeded: false, commitCount: 0 };
|
|
256
256
|
if (getAgentStatus(workspace.id) !== null)
|
|
257
257
|
throw new Error('Cannot change the source branch while the agent is running');
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
try {
|
|
259
|
+
await runBoundedProcess('bash', ['-c', script], {
|
|
260
260
|
cwd: workspace.worktreePath,
|
|
261
|
+
timeoutMs: SCRIPT_TIMEOUT_MS,
|
|
261
262
|
env: {
|
|
262
263
|
...process.env,
|
|
263
264
|
KOBO_NEW_BASE: newBase,
|
|
@@ -271,46 +272,13 @@ async function runCustomScript(workspace, oldBase, newBase, script) {
|
|
|
271
272
|
KOBO_FORGE: forgeId,
|
|
272
273
|
KOBO_PR_NUMBER: prNumber,
|
|
273
274
|
},
|
|
274
|
-
// Detached so `child` leads its own process group. Node's native
|
|
275
|
-
// `timeout` option below only signals `child` itself — without this,
|
|
276
|
-
// a script that backgrounds a long-running command (`docker compose
|
|
277
|
-
// up -d &`) leaves it running as an orphan after the timeout fires.
|
|
278
|
-
detached: true,
|
|
279
|
-
timeout: SCRIPT_TIMEOUT_MS,
|
|
280
|
-
killSignal: 'SIGTERM',
|
|
281
|
-
});
|
|
282
|
-
// Node's `timeout` option kills only `child`. Mirror that same signal to
|
|
283
|
-
// the whole process group so a backgrounded child dies with it.
|
|
284
|
-
const groupKillTimer = setTimeout(() => {
|
|
285
|
-
try {
|
|
286
|
-
if (child.pid)
|
|
287
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
/* process group already gone */
|
|
291
|
-
}
|
|
292
|
-
}, SCRIPT_TIMEOUT_MS);
|
|
293
|
-
let stderrBuf = '';
|
|
294
|
-
child.stderr?.on('data', (chunk) => {
|
|
295
|
-
stderrBuf += chunk.toString();
|
|
296
|
-
if (stderrBuf.length > 8 * 1024)
|
|
297
|
-
stderrBuf = stderrBuf.slice(-8 * 1024);
|
|
298
|
-
});
|
|
299
|
-
child.on('error', (err) => {
|
|
300
|
-
clearTimeout(groupKillTimer);
|
|
301
|
-
reject(new Error(`Custom change-source-branch script failed to spawn: ${err.message}`));
|
|
302
275
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
const detail = stderrBuf.trim().slice(-500) || `exit code ${code ?? signal ?? 'unknown'}`;
|
|
311
|
-
reject(new Error(`Custom change-source-branch script failed: ${detail}`));
|
|
312
|
-
});
|
|
313
|
-
});
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
throw new Error(`Custom change-source-branch script failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
}
|
|
280
|
+
updateWorkspaceSourceBranch(workspace.id, newBase);
|
|
281
|
+
return { status: 'done', forcePushNeeded: false, commitCount: 0 };
|
|
314
282
|
}
|
|
315
283
|
/** Async lookups must not authorize mutations using obsolete workspace metadata. */
|
|
316
284
|
function assertWorkspaceUnchanged(id, worktreePath, branch, sourceBranch) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { emit, emitEphemeral } from './websocket-service.js';
|
|
2
|
+
/** Correlate delivery confirmations without changing the legacy queue protocol. */
|
|
3
|
+
export function createChatReceipt(workspaceId, delivery, persist) {
|
|
4
|
+
const correlation = delivery.clientMessageId ? { clientMessageId: delivery.clientMessageId } : {};
|
|
5
|
+
return {
|
|
6
|
+
accept(sessionId) {
|
|
7
|
+
const payload = {
|
|
8
|
+
content: delivery.content,
|
|
9
|
+
sender: 'user',
|
|
10
|
+
...correlation,
|
|
11
|
+
...(delivery.source ? { source: delivery.source } : {}),
|
|
12
|
+
};
|
|
13
|
+
if (persist)
|
|
14
|
+
persist(payload, sessionId);
|
|
15
|
+
else if (delivery.clientMessageId)
|
|
16
|
+
emit(workspaceId, 'user:message', payload, sessionId, { requirePersistence: true });
|
|
17
|
+
else
|
|
18
|
+
emit(workspaceId, 'user:message', payload, sessionId);
|
|
19
|
+
if (delivery.force || delivery.clientMessageId) {
|
|
20
|
+
emitEphemeral(workspaceId, 'chat:accepted', { sessionId, ...correlation });
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
reject(message, reason) {
|
|
24
|
+
emitEphemeral(workspaceId, 'chat:rejected', {
|
|
25
|
+
sessionId: delivery.sessionId,
|
|
26
|
+
content: delivery.content,
|
|
27
|
+
message,
|
|
28
|
+
...correlation,
|
|
29
|
+
...(reason ? { reason } : {}),
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -63,8 +63,22 @@ function rowToCron(row) {
|
|
|
63
63
|
lastFiredAt: row.last_fired_at,
|
|
64
64
|
oneShot: row.one_shot === 1,
|
|
65
65
|
createdAt: row.created_at,
|
|
66
|
+
...(expressionError(row.expression) ? { validationError: expressionError(row.expression) } : {}),
|
|
66
67
|
};
|
|
67
68
|
}
|
|
69
|
+
function expressionError(expression) {
|
|
70
|
+
const trimmed = expression.trim();
|
|
71
|
+
if (!['@hourly', '@daily', '@weekly', '@monthly', '@yearly'].includes(trimmed) && trimmed.split(/\s+/).length !== 5) {
|
|
72
|
+
return `Invalid cron expression: ${expression} — expected five fields or a supported alias`;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
CronExpressionParser.parse(trimmed);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return `Invalid cron expression: ${expression} — ${err instanceof Error ? err.message : String(err)}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
68
82
|
/**
|
|
69
83
|
* Compute the next fire time for an expression strictly after `from`.
|
|
70
84
|
* Uses cron-parser 5.x API. Throws with a descriptive error if the
|
|
@@ -72,6 +86,9 @@ function rowToCron(row) {
|
|
|
72
86
|
* `@monthly` / `@yearly` are accepted natively.
|
|
73
87
|
*/
|
|
74
88
|
function nextAfter(expression, from) {
|
|
89
|
+
const validationError = expressionError(expression);
|
|
90
|
+
if (validationError)
|
|
91
|
+
throw new Error(validationError);
|
|
75
92
|
try {
|
|
76
93
|
const it = CronExpressionParser.parse(expression, { currentDate: from });
|
|
77
94
|
return it.next().toDate();
|
|
@@ -173,6 +190,8 @@ function fireOrSkip(id) {
|
|
|
173
190
|
const row = db.prepare('SELECT * FROM pending_crons WHERE id = ?').get(id);
|
|
174
191
|
if (!row)
|
|
175
192
|
return; // cancelled in flight
|
|
193
|
+
if (expressionError(row.expression))
|
|
194
|
+
return; // Retain invalid legacy intent without executing it.
|
|
176
195
|
const wsRow = db
|
|
177
196
|
.prepare(`SELECT project_path, working_branch, worktree_path, model, agent_permission_mode, reasoning_effort, archived_at
|
|
178
197
|
FROM workspaces WHERE id = ?`)
|
|
@@ -212,6 +231,9 @@ function fireOrSkip(id) {
|
|
|
212
231
|
const now = new Date();
|
|
213
232
|
let nextFire;
|
|
214
233
|
try {
|
|
234
|
+
// Valid five-field schedules have at most one occurrence per minute.
|
|
235
|
+
// Skip missed slots from now, without adding a minute of timer jitter
|
|
236
|
+
// that would discard the next scheduled occurrence too.
|
|
215
237
|
nextFire = nextAfter(row.expression, now);
|
|
216
238
|
}
|
|
217
239
|
catch (err) {
|
|
@@ -220,7 +242,6 @@ function fireOrSkip(id) {
|
|
|
220
242
|
// arm and this fire. Cancel as 'completed' (the cron self-terminates)
|
|
221
243
|
// rather than 'user' which would imply the user requested it.
|
|
222
244
|
console.error(`[cron-service] failed to recompute next fire for cron '${id}':`, err);
|
|
223
|
-
cancel(id, 'completed');
|
|
224
245
|
return;
|
|
225
246
|
}
|
|
226
247
|
// One-shot crons cancel themselves after a real fire (not on skip-active —
|
|
@@ -278,6 +299,11 @@ export function restoreOnBoot() {
|
|
|
278
299
|
db.prepare('DELETE FROM pending_crons WHERE id = ?').run(row.id);
|
|
279
300
|
continue;
|
|
280
301
|
}
|
|
302
|
+
const validationError = expressionError(row.expression);
|
|
303
|
+
if (validationError) {
|
|
304
|
+
console.warn(`[cron-service] ${validationError}; cron ${row.id} remains inactive`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
281
307
|
const storedDate = new Date(row.next_fire_at);
|
|
282
308
|
let nextFireAt;
|
|
283
309
|
if (storedDate.getTime() <= now.getTime()) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { normalizeClientName } from '../../shared/workspace-message-types.js';
|
|
2
|
+
export function createMcpClientContext(name, transport = 'http', encoding) {
|
|
3
|
+
if (encoding === 'uri' && typeof name === 'string') {
|
|
4
|
+
try {
|
|
5
|
+
name = decodeURIComponent(name);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
name = undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return { kind: 'mcp', clientName: normalizeClientName(name), transport };
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { getCompiledMcpServerPath, getDbPath, getMcpServerSourcePath } from '../utils/paths.js';
|
|
6
|
+
import { getLanHostnames, isAllowedRequestHost, resolveProxyHostname } from './network-access-service.js';
|
|
7
|
+
/** No request/forwarded headers or credentials participate in generated metadata. */
|
|
8
|
+
export function buildMcpConnectionInfo(input) {
|
|
9
|
+
const backendUrl = `http://127.0.0.1:${input.port}`;
|
|
10
|
+
const args = input.compiledPath
|
|
11
|
+
? [input.compiledPath]
|
|
12
|
+
: input.sourcePath && input.loaderPath
|
|
13
|
+
? ['--import', pathToFileURL(input.loaderPath).href, input.sourcePath]
|
|
14
|
+
: null;
|
|
15
|
+
return {
|
|
16
|
+
localUrl: `${backendUrl}/api/mcp`,
|
|
17
|
+
lanUrls: input.networkEnabled
|
|
18
|
+
? input.lanHostnames
|
|
19
|
+
.map((host) => `${host.includes(':') ? `[${host}]` : host}:${input.port}`)
|
|
20
|
+
.filter((host) => isAllowedRequestHost({
|
|
21
|
+
host,
|
|
22
|
+
enabled: input.networkEnabled,
|
|
23
|
+
lanHostnames: input.lanHostnames,
|
|
24
|
+
behindProxy: input.behindProxy,
|
|
25
|
+
proxyHostname: input.proxyHostname,
|
|
26
|
+
}))
|
|
27
|
+
.map((host) => `http://${host}/api/mcp`)
|
|
28
|
+
: [],
|
|
29
|
+
networkEnabled: input.networkEnabled,
|
|
30
|
+
behindProxy: input.behindProxy,
|
|
31
|
+
proxyHostname: input.proxyHostname,
|
|
32
|
+
localRequiresToken: input.behindProxy,
|
|
33
|
+
stdio: args
|
|
34
|
+
? {
|
|
35
|
+
command: input.command,
|
|
36
|
+
args,
|
|
37
|
+
env: { KOBO_DB_PATH: input.databasePath, KOBO_BACKEND_URL: backendUrl, KOBO_WORKSPACE_ID: '' },
|
|
38
|
+
}
|
|
39
|
+
: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function getMcpConnectionInfo(port, settings, databasePath = getDbPath()) {
|
|
43
|
+
const source = getMcpServerSourcePath();
|
|
44
|
+
let loaderPath = null;
|
|
45
|
+
try {
|
|
46
|
+
loaderPath = createRequire(import.meta.url).resolve('tsx');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* Production installs can omit the development loader. */
|
|
50
|
+
}
|
|
51
|
+
return buildMcpConnectionInfo({
|
|
52
|
+
port,
|
|
53
|
+
networkEnabled: settings.networkAccessEnabled,
|
|
54
|
+
behindProxy: settings.networkAccessBehindProxy,
|
|
55
|
+
proxyHostname: resolveProxyHostname(),
|
|
56
|
+
lanHostnames: getLanHostnames(),
|
|
57
|
+
command: process.execPath,
|
|
58
|
+
compiledPath: getCompiledMcpServerPath(),
|
|
59
|
+
sourcePath: fs.existsSync(source) ? source : null,
|
|
60
|
+
loaderPath,
|
|
61
|
+
databasePath: path.resolve(databasePath),
|
|
62
|
+
});
|
|
63
|
+
}
|