@loicngr/kobo 1.11.39 → 1.11.41
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 +101 -32
- package/CHANGELOG.md +8 -0
- package/README.md +6 -4
- 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/images.js +65 -5
- 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 +250 -248
- 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/attachment-service.js +122 -0
- 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/image-service.js +16 -7
- 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/attachments.js +58 -0
- 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-DTIe6O9o.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BWeu0YeI.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-o_EHlTwV.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-CGi88k6z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DbY5gBv8.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CIo_gesD.js +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DEcf48KI.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-D9vcB91W.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-Ca0_CxSl.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DCmB2R5i.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-_q3vy_xn.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Duwnq3cc.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-BeiZIHVA.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-Fj3M9QGD.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DtSUuK8t.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-DwkX8eQ6.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BlZDd7TK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGi1yRxU.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-DnO-ImWE.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-vz6jN1DM.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-IdPIGp2_.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DTJqxVp9.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-Bj2GGyhU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BBaGZlwv.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Dy0spsfi.js +4 -0
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-BFj5wwbS.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-C-HSiySk.js} +1 -1
- package/src/client/dist/pwa/assets/de-BdLe7apJ.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-C2Xnjh9Z.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-BbKfnNmv.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-BM7fdaaL.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-b74YRODT.js +1 -0
- package/src/client/dist/pwa/assets/es-C1LM-3aq.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-DiDWiIkx.js +1 -0
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-CnbUxAmw.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-B2uUIbAY.js} +1 -1
- package/src/client/dist/pwa/assets/fr-KCK6r5ga.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-Cu1vGY0x.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-CETuqLyz.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-BTNpr39P.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BkUirOzl.js +1 -0
- package/src/client/dist/pwa/assets/index-mvh5w1cM.js +20 -0
- package/src/client/dist/pwa/assets/it-BM6t0IGU.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C7Ncb6V7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-D0hUdg4x.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-BK5H1Xst.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-BBW6JNgz.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-1LZrAlY4.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-CVGS__6a.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-DIKLxr8F.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-B02zuH1j.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-C3otN1Vk.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-BUcW_eZA.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DEeUVvnp.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-CG6cL8ya.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-Ct_T5TPN.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-CSQAg5UC.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-_6HwJ_Rp.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-B8WWsWEI.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-BSB4-DXR.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-CSGKph85.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 +169 -9
- 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-A3pVradx.css +0 -1
- 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-BAC6u9OP.css +0 -1
- 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/expand-template-B6xAlAaM.js +0 -1
- 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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Derived data only. The event log remains the source of truth. */
|
|
2
|
+
export function initSearchSchema(db) {
|
|
3
|
+
db.exec(`
|
|
4
|
+
CREATE TABLE IF NOT EXISTS search_index_state (
|
|
5
|
+
id INTEGER PRIMARY KEY CHECK(id = 1), cursor TEXT NOT NULL DEFAULT '',
|
|
6
|
+
complete INTEGER NOT NULL DEFAULT 0, processed INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
total INTEGER NOT NULL DEFAULT -1, base_order INTEGER NOT NULL
|
|
8
|
+
);
|
|
9
|
+
INSERT OR IGNORE INTO search_index_state(id, base_order)
|
|
10
|
+
SELECT 1, COALESCE(MAX(rowid), 0) FROM ws_events;
|
|
11
|
+
CREATE TABLE IF NOT EXISTS search_changes (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT NOT NULL, operation TEXT NOT NULL
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_search_changes_event ON search_changes(event_id);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS search_fragments (
|
|
16
|
+
event_id TEXT PRIMARY KEY, message_key TEXT NOT NULL,
|
|
17
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
18
|
+
session_id TEXT, type TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
19
|
+
event_order INTEGER NOT NULL, text TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_search_fragments_message ON search_fragments(message_key, event_order);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS search_messages (
|
|
23
|
+
id INTEGER PRIMARY KEY, message_key TEXT NOT NULL UNIQUE,
|
|
24
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
25
|
+
session_id TEXT, event_id TEXT NOT NULL, type TEXT NOT NULL,
|
|
26
|
+
created_at TEXT NOT NULL, text TEXT NOT NULL, normalized TEXT NOT NULL
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_search_messages_workspace ON search_messages(workspace_id, created_at DESC);
|
|
29
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_messages_fts USING fts5(
|
|
30
|
+
normalized, content='search_messages', content_rowid='id', tokenize='trigram case_sensitive 1'
|
|
31
|
+
);
|
|
32
|
+
CREATE TRIGGER IF NOT EXISTS search_messages_insert AFTER INSERT ON search_messages BEGIN
|
|
33
|
+
INSERT INTO search_messages_fts(rowid, normalized) VALUES (new.id, new.normalized);
|
|
34
|
+
END;
|
|
35
|
+
CREATE TRIGGER IF NOT EXISTS search_messages_delete AFTER DELETE ON search_messages BEGIN
|
|
36
|
+
INSERT INTO search_messages_fts(search_messages_fts, rowid, normalized) VALUES ('delete', old.id, old.normalized);
|
|
37
|
+
END;
|
|
38
|
+
CREATE TRIGGER IF NOT EXISTS search_messages_update AFTER UPDATE ON search_messages BEGIN
|
|
39
|
+
INSERT INTO search_messages_fts(search_messages_fts, rowid, normalized) VALUES ('delete', old.id, old.normalized);
|
|
40
|
+
INSERT INTO search_messages_fts(rowid, normalized) VALUES (new.id, new.normalized);
|
|
41
|
+
END;
|
|
42
|
+
CREATE TRIGGER IF NOT EXISTS search_events_insert AFTER INSERT ON ws_events BEGIN
|
|
43
|
+
INSERT INTO search_changes(event_id, operation) VALUES(new.id, 'insert');
|
|
44
|
+
END;
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS search_events_update AFTER UPDATE ON ws_events BEGIN
|
|
46
|
+
INSERT INTO search_changes(event_id, operation) VALUES(old.id, 'update');
|
|
47
|
+
INSERT INTO search_changes(event_id, operation) SELECT new.id, 'update' WHERE new.id != old.id;
|
|
48
|
+
END;
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS search_events_delete AFTER DELETE ON ws_events BEGIN
|
|
50
|
+
INSERT INTO search_changes(event_id, operation) VALUES(old.id, 'delete');
|
|
51
|
+
END;
|
|
52
|
+
`);
|
|
53
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import fsRouter from './routes/fs.js';
|
|
|
18
18
|
import gitRouter from './routes/git.js';
|
|
19
19
|
import healthRouter from './routes/health.js';
|
|
20
20
|
import imagesRouter from './routes/images.js';
|
|
21
|
+
import mcpRouter from './routes/mcp.js';
|
|
21
22
|
import { migrationRouter } from './routes/migration.js';
|
|
22
23
|
import notionRouter from './routes/notion.js';
|
|
23
24
|
import pullRequestsRouter from './routes/pull-requests.js';
|
|
@@ -29,23 +30,27 @@ import usageRoutes from './routes/usage.js';
|
|
|
29
30
|
import voiceRouter from './routes/voice.js';
|
|
30
31
|
import workspaceTemplatesRouter from './routes/workspace-templates.js';
|
|
31
32
|
import workspacesRouter from './routes/workspaces.js';
|
|
32
|
-
import { getAvailableSkills,
|
|
33
|
+
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
33
34
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
34
35
|
import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
|
|
35
36
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
36
37
|
import * as cronService from './services/cron-service.js';
|
|
37
38
|
import { createDailyDbBackupIfNeeded, createPreMigrationBackup, startDailyDbBackupScheduler, } from './services/db-backup-service.js';
|
|
38
39
|
import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
|
|
40
|
+
import { reconcileMessageRequests } from './services/mcp-message-request-service.js';
|
|
39
41
|
import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
|
|
40
42
|
import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
|
|
41
43
|
import * as quotaBackoffService from './services/quota-backoff-service.js';
|
|
44
|
+
import { startSearchIndex, stopSearchIndex } from './services/search-service.js';
|
|
42
45
|
import { getGlobalSettings, updateNetworkAccessSettings } from './services/settings-service.js';
|
|
43
46
|
import { reloadDefaultTemplates } from './services/templates-service.js';
|
|
44
47
|
import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
|
|
48
|
+
import { startUpdateChecker, stopUpdateChecker } from './services/update-check-service.js';
|
|
45
49
|
import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
46
50
|
import * as wakeupService from './services/wakeup-service.js';
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
51
|
+
import { handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
52
|
+
import { deliverWorkspaceMessage } from './services/workspace-message-service.js';
|
|
53
|
+
import { getWorkspace } from './services/workspace-service.js';
|
|
49
54
|
import { pruneWsEvents, resolveRetentionConfig } from './services/ws-events-retention-service.js';
|
|
50
55
|
import { getChangelogPath, getClientSpaPath, getDbPath, getKoboHome, getPackageVersion, resolveSpaFile, } from './utils/paths.js';
|
|
51
56
|
import { formatStartupBanner, readStartupNotes, startStartupSpinner, startupDebug } from './utils/startup-display.js';
|
|
@@ -69,6 +74,8 @@ catch (err) {
|
|
|
69
74
|
console.error('[kobo] Pre-migration backup failed (continuing — daily backup remains as fallback):', err);
|
|
70
75
|
}
|
|
71
76
|
runMigrations(db);
|
|
77
|
+
reconcileMessageRequests(db);
|
|
78
|
+
startSearchIndex();
|
|
72
79
|
// Event retention. OPT-IN: disabled by default, so this is a no-op until the
|
|
73
80
|
// user sets a window in Settings → Worktrees. Deliberately not enabled by
|
|
74
81
|
// default — turning it on for an existing install would delete months of
|
|
@@ -145,6 +152,7 @@ app.use('/api/*', networkAuthMiddleware);
|
|
|
145
152
|
app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
|
|
146
153
|
// Mount route sub-routers
|
|
147
154
|
app.route('/api/workspaces', workspacesRouter);
|
|
155
|
+
app.route('/api/mcp', mcpRouter);
|
|
148
156
|
app.route('/api/pull-requests', pullRequestsRouter);
|
|
149
157
|
app.route('/api/workspaces', imagesRouter);
|
|
150
158
|
app.route('/api/notion', notionRouter);
|
|
@@ -221,6 +229,7 @@ const server = serve({
|
|
|
221
229
|
hostname: bindHost,
|
|
222
230
|
}, (info) => {
|
|
223
231
|
setBackendPort(info.port);
|
|
232
|
+
startUpdateChecker();
|
|
224
233
|
stopStartupSpinner();
|
|
225
234
|
const settings = getGlobalSettings();
|
|
226
235
|
console.log(formatStartupBanner({
|
|
@@ -346,91 +355,11 @@ terminalWss.on('connection', (ws, workspaceId) => {
|
|
|
346
355
|
setMessageHandler(async (type, payload) => {
|
|
347
356
|
const p = payload;
|
|
348
357
|
if (type === 'chat:message' && p?.workspaceId && p?.content) {
|
|
349
|
-
if (getWorkspace(p.workspaceId)?.status === 'compacting') {
|
|
350
|
-
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
351
|
-
reason: 'compacting',
|
|
352
|
-
sessionId: p.sessionId,
|
|
353
|
-
content: p.content,
|
|
354
|
-
message: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
355
|
-
});
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
// Auto-loop owns the agent's turns. A user message means the user wants
|
|
359
|
-
// to redirect the conversation, so disable the loop (idempotent — the
|
|
360
|
-
// `autoloop:disabled` event is emitted with reason='user-action' so the
|
|
361
|
-
// frontend chip updates) and let the message through to the running
|
|
362
|
-
// session. The user can re-enable auto-loop manually once their
|
|
363
|
-
// intervention is done. Grooming phase (ready=0) is skipped — the loop
|
|
364
|
-
// hasn't started yet, so chat messages during grooming pass through
|
|
365
|
-
// untouched (the user can still answer the agent's questions).
|
|
366
|
-
const autoLoopStatus = autoLoopService.getStatus(p.workspaceId);
|
|
367
|
-
if (autoLoopStatus.auto_loop && autoLoopStatus.auto_loop_ready) {
|
|
368
|
-
autoLoopService.disable(p.workspaceId, 'user-action');
|
|
369
|
-
}
|
|
370
|
-
// Reject chat input while paused on canUseTool — sending here would spawn
|
|
371
|
-
// a parallel session and orphan the pending callback.
|
|
372
|
-
const wsRow = getWorkspace(p.workspaceId);
|
|
373
|
-
if (wsRow?.status === 'awaiting-user') {
|
|
374
|
-
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
375
|
-
reason: 'awaiting-user',
|
|
376
|
-
sessionId: p.sessionId,
|
|
377
|
-
content: p.content,
|
|
378
|
-
message: 'Answer via the question panel — typing in chat would orphan the pending callback',
|
|
379
|
-
});
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
// Prefer the session explicitly selected by the client (sessionId hint),
|
|
383
|
-
// falling back to the running/most-recent non-idle session so idle sessions
|
|
384
|
-
// never steal the tagging.
|
|
385
|
-
const activeSession = getActiveSession(p.workspaceId);
|
|
386
|
-
const sessionTag = p.sessionId ?? activeSession?.id ?? undefined;
|
|
387
358
|
try {
|
|
388
|
-
await
|
|
389
|
-
emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
|
|
390
|
-
if (p.force)
|
|
391
|
-
emitEphemeral(p.workspaceId, 'chat:accepted', { sessionId: p.sessionId });
|
|
359
|
+
await deliverWorkspaceMessage(p.workspaceId, { ...p, content: p.content });
|
|
392
360
|
}
|
|
393
|
-
catch (
|
|
394
|
-
|
|
395
|
-
// Resume on every shape that means "no agent can receive this message"
|
|
396
|
-
// — the closed-stdin case included. Anything else (queue full, disk
|
|
397
|
-
// error, a stop in progress) surfaces to the user instead of silently
|
|
398
|
-
// respawning a fresh agent.
|
|
399
|
-
if (!isAgentUnavailableError(msg)) {
|
|
400
|
-
emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, content: p.content, message: msg });
|
|
401
|
-
console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, err);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
// Agent not running — resume the session hinted by the client if any,
|
|
405
|
-
// otherwise the most-recent active session.
|
|
406
|
-
try {
|
|
407
|
-
const workspace = getWorkspace(p.workspaceId);
|
|
408
|
-
if (workspace) {
|
|
409
|
-
const worktreePath = workspace.worktreePath;
|
|
410
|
-
// Plan mode blocks MCP tools — when the caller knows the message
|
|
411
|
-
// requires them (e.g. grooming), it sets the override to bypass the
|
|
412
|
-
// workspace default for this spawn only.
|
|
413
|
-
const effectiveMode = p.agentPermissionModeOverride ?? workspace.agentPermissionMode;
|
|
414
|
-
const started = startAgent(p.workspaceId, worktreePath, p.content, workspace.model, true, effectiveMode, p.sessionId, workspace.reasoningEffort);
|
|
415
|
-
updateWorkspaceStatus(p.workspaceId, 'executing');
|
|
416
|
-
const resumedSessionId = p.sessionId ?? started.agentSessionId;
|
|
417
|
-
emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, resumedSessionId);
|
|
418
|
-
if (p.force)
|
|
419
|
-
emitEphemeral(p.workspaceId, 'chat:accepted', { sessionId: resumedSessionId });
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
423
|
-
sessionId: p.sessionId,
|
|
424
|
-
content: p.content,
|
|
425
|
-
message: `Workspace '${p.workspaceId}' not found`,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
catch (restartErr) {
|
|
430
|
-
const message = restartErr instanceof Error ? restartErr.message : String(restartErr);
|
|
431
|
-
emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, content: p.content, message });
|
|
432
|
-
console.error('[ws] Failed to resume agent:', message);
|
|
433
|
-
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, error);
|
|
434
363
|
}
|
|
435
364
|
}
|
|
436
365
|
if (type === 'workspace:start' && p?.workspaceId) {
|
|
@@ -543,6 +472,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
543
472
|
if (isShuttingDown)
|
|
544
473
|
return;
|
|
545
474
|
isShuttingDown = true;
|
|
475
|
+
stopUpdateChecker();
|
|
546
476
|
console.log(`\n[kobo] Received ${signal}, shutting down gracefully…`);
|
|
547
477
|
const forceExitTimer = setTimeout(() => {
|
|
548
478
|
console.error('[kobo] Graceful shutdown timed out; forcing exit');
|
|
@@ -608,6 +538,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
608
538
|
}
|
|
609
539
|
finally {
|
|
610
540
|
try {
|
|
541
|
+
await stopSearchIndex();
|
|
611
542
|
await dailyBackupScheduler.stop();
|
|
612
543
|
closeDb();
|
|
613
544
|
console.log('[kobo] Database closed');
|
|
@@ -21,11 +21,15 @@ export const networkAuthMiddleware = async (c, next) => {
|
|
|
21
21
|
return next();
|
|
22
22
|
const address = getConnInfo(c).remote.address;
|
|
23
23
|
const global = getGlobalSettings();
|
|
24
|
+
// MCP clients commonly send the shared token as an Authorization header.
|
|
25
|
+
const bearerToken = /^\/api\/mcp\/?$/.test(c.req.path)
|
|
26
|
+
? c.req.header('Authorization')?.match(/^Bearer\s+(.+)$/i)?.[1]
|
|
27
|
+
: undefined;
|
|
24
28
|
const decision = evaluateNetworkAccess({
|
|
25
29
|
address,
|
|
26
30
|
enabled: global.networkAccessEnabled,
|
|
27
31
|
expectedToken: global.networkAccessToken,
|
|
28
|
-
providedToken: c.req.header('X-Kobo-Token'),
|
|
32
|
+
providedToken: c.req.header('X-Kobo-Token') ?? bearerToken,
|
|
29
33
|
trustLoopback: !global.networkAccessBehindProxy,
|
|
30
34
|
});
|
|
31
35
|
if (decision.allow)
|
|
@@ -1,57 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { refreshUpdateCheck } from '../services/update-check-service.js';
|
|
3
4
|
import { parseChangelog } from '../utils/changelog.js';
|
|
4
|
-
import { getChangelogPath
|
|
5
|
+
import { getChangelogPath } from '../utils/paths.js';
|
|
6
|
+
export { _clearLatestVersionCache } from '../services/update-check-service.js';
|
|
5
7
|
export { parseChangelog } from '../utils/changelog.js';
|
|
6
8
|
/** Hono sub-router for the in-app "What's new" dialog. */
|
|
7
9
|
const app = new Hono();
|
|
8
|
-
/**
|
|
9
|
-
* Latest version published to npm, looked up at most once a day.
|
|
10
|
-
*
|
|
11
|
-
* "What's new" tells the user what changed once they have upgraded; nothing
|
|
12
|
-
* told them an upgrade existed. A self-hosted install would silently rot.
|
|
13
|
-
*
|
|
14
|
-
* Best-effort by construction: no network, a registry hiccup or an air-gapped
|
|
15
|
-
* machine all resolve to null, and the client simply shows nothing.
|
|
16
|
-
*/
|
|
17
|
-
const REGISTRY_URL = 'https://registry.npmjs.org/@loicngr/kobo/latest';
|
|
18
|
-
const LATEST_TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
-
let latestCache = null;
|
|
20
|
-
/** The lookup in progress, so two loads racing on a cold cache share one request. */
|
|
21
|
-
let inFlight = null;
|
|
22
|
-
async function fetchLatestVersion() {
|
|
23
|
-
if (latestCache && Date.now() - latestCache.fetchedAt < LATEST_TTL_MS)
|
|
24
|
-
return latestCache.version;
|
|
25
|
-
if (inFlight)
|
|
26
|
-
return inFlight;
|
|
27
|
-
inFlight = lookupLatestVersion().finally(() => {
|
|
28
|
-
inFlight = null;
|
|
29
|
-
});
|
|
30
|
-
return inFlight;
|
|
31
|
-
}
|
|
32
|
-
async function lookupLatestVersion() {
|
|
33
|
-
let version = null;
|
|
34
|
-
try {
|
|
35
|
-
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5_000) });
|
|
36
|
-
if (res.ok) {
|
|
37
|
-
const body = (await res.json());
|
|
38
|
-
if (typeof body.version === 'string')
|
|
39
|
-
version = body.version;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Offline, blocked, or the registry is down. Not worth a log line on a
|
|
44
|
-
// path that runs on every app load.
|
|
45
|
-
}
|
|
46
|
-
// Cache the failure too: a machine with no network must not retry per load.
|
|
47
|
-
latestCache = { version, fetchedAt: Date.now() };
|
|
48
|
-
return version;
|
|
49
|
-
}
|
|
50
|
-
/** @internal test-only — drop the cached registry lookup. */
|
|
51
|
-
export function _clearLatestVersionCache() {
|
|
52
|
-
latestCache = null;
|
|
53
|
-
inFlight = null;
|
|
54
|
-
}
|
|
55
10
|
// GET /api/changelog — current app version + parsed CHANGELOG.md sections,
|
|
56
11
|
// powering the "What's new" dialog shown after an update, plus the latest
|
|
57
12
|
// version published to npm so the UI can offer to upgrade.
|
|
@@ -64,7 +19,7 @@ app.get('/', async (c) => {
|
|
|
64
19
|
catch {
|
|
65
20
|
// No CHANGELOG.md shipped (or unreadable) — degrade to an empty list.
|
|
66
21
|
}
|
|
67
|
-
return c.json({
|
|
22
|
+
return c.json({ ...(await refreshUpdateCheck()), versions });
|
|
68
23
|
}
|
|
69
24
|
catch (err) {
|
|
70
25
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -42,7 +42,7 @@ app.get('/files', (c) => {
|
|
|
42
42
|
});
|
|
43
43
|
// GET /api/git/orphan-worktrees?projectPath=<path> — list worktrees of a
|
|
44
44
|
// project that are NOT attached to any Kōbō workspace yet.
|
|
45
|
-
app.get('/orphan-worktrees', (c) => {
|
|
45
|
+
app.get('/orphan-worktrees', async (c) => {
|
|
46
46
|
try {
|
|
47
47
|
const projectPath = c.req.query('projectPath');
|
|
48
48
|
if (!projectPath) {
|
|
@@ -51,7 +51,7 @@ app.get('/orphan-worktrees', (c) => {
|
|
|
51
51
|
const db = getDb();
|
|
52
52
|
const rows = db.prepare('SELECT worktree_path FROM workspaces WHERE project_path = ?').all(projectPath);
|
|
53
53
|
const attachedPaths = new Set(rows.map((r) => r.worktree_path).filter((p) => !!p));
|
|
54
|
-
const orphans = listOrphanWorktrees(projectPath, attachedPaths);
|
|
54
|
+
const orphans = await listOrphanWorktrees(projectPath, attachedPaths);
|
|
55
55
|
return c.json(orphans);
|
|
56
56
|
}
|
|
57
57
|
catch (err) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
5
|
+
import { DOCUMENT_EXTENSIONS, MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_REQUEST_BYTES, validateAttachments, } from '../../shared/attachments.js';
|
|
6
|
+
import { attachmentReference, saveAttachments } from '../services/attachment-service.js';
|
|
4
7
|
import * as imageService from '../services/image-service.js';
|
|
5
8
|
import * as workspaceService from '../services/workspace-service.js';
|
|
6
|
-
import { isPathInside } from '../utils/safe-path.js';
|
|
7
|
-
|
|
8
|
-
const MAX_FILE_SIZE =
|
|
9
|
+
import { ensureDirectoryInside, isPathInside } from '../utils/safe-path.js';
|
|
10
|
+
import { WorkspaceLifecycleBusyError, withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
11
|
+
const MAX_FILE_SIZE = MAX_ATTACHMENT_BYTES;
|
|
9
12
|
/** MIME types accepted for image uploads. */
|
|
10
13
|
const ALLOWED_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
11
14
|
/** File extension → MIME type, used to serve uploaded images back to the frontend. */
|
|
@@ -18,8 +21,65 @@ const EXT_TO_MIME = {
|
|
|
18
21
|
};
|
|
19
22
|
/** Hono sub-router for workspace image upload and deletion. */
|
|
20
23
|
const app = new Hono();
|
|
24
|
+
// New chat uploads share formats, limits and storage with workspace creation.
|
|
25
|
+
app.post('/:id/attachments', bodyLimit({ maxSize: MAX_ATTACHMENT_REQUEST_BYTES }), async (c) => {
|
|
26
|
+
try {
|
|
27
|
+
const { id } = c.req.param();
|
|
28
|
+
let form;
|
|
29
|
+
try {
|
|
30
|
+
form = await c.req.raw.formData();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return c.json({ error: 'Invalid multipart body' }, 400);
|
|
34
|
+
}
|
|
35
|
+
const files = form.getAll('attachment');
|
|
36
|
+
if (files.length !== 1 || !(files[0] instanceof File))
|
|
37
|
+
return c.json({ error: 'Expected one attachment file' }, 400);
|
|
38
|
+
const file = files[0];
|
|
39
|
+
const error = validateAttachments([file]);
|
|
40
|
+
if (error)
|
|
41
|
+
return c.json({ error: `Invalid attachment: ${error}`, code: error }, 400);
|
|
42
|
+
return await withWorkspaceLifecycleGuard(id, async () => {
|
|
43
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
44
|
+
if (!workspace)
|
|
45
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
46
|
+
if (workspace.worktreePurgedAt)
|
|
47
|
+
return c.json({ error: 'Restore the worktree before uploading files' }, 409);
|
|
48
|
+
const [saved] = await saveAttachments(workspace.worktreePath, [file]);
|
|
49
|
+
return c.json({ ...saved, path: saved.relativePath, reference: attachmentReference(saved) }, 201);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, error instanceof WorkspaceLifecycleBusyError ? 409 : 500);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
app.delete('/:id/attachments/:filename', async (c) => {
|
|
57
|
+
try {
|
|
58
|
+
const { id, filename } = c.req.param();
|
|
59
|
+
// Accept only the generated document layout, never a caller-supplied path.
|
|
60
|
+
if (!/^[A-Za-z0-9_-]{10}\.[a-z]+$/.test(filename) ||
|
|
61
|
+
!DOCUMENT_EXTENSIONS.includes(path.extname(filename))) {
|
|
62
|
+
return c.json({ error: 'Invalid attachment filename' }, 400);
|
|
63
|
+
}
|
|
64
|
+
return await withWorkspaceLifecycleGuard(id, async () => {
|
|
65
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
66
|
+
if (!workspace)
|
|
67
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
68
|
+
if (workspace.worktreePurgedAt)
|
|
69
|
+
return c.json({ error: 'Worktree is purged' }, 409);
|
|
70
|
+
const directory = ensureDirectoryInside(workspace.worktreePath, '.ai/attachments');
|
|
71
|
+
// unlink removes a replaced leaf symlink itself; it never follows it.
|
|
72
|
+
fs.unlinkSync(path.join(directory, filename));
|
|
73
|
+
return c.body(null, 204);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const missing = error.code === 'ENOENT';
|
|
78
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, error instanceof WorkspaceLifecycleBusyError ? 409 : missing ? 404 : 500);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
21
81
|
// POST /:id/images — upload an image
|
|
22
|
-
app.post('/:id/images', async (c) => {
|
|
82
|
+
app.post('/:id/images', bodyLimit({ maxSize: MAX_ATTACHMENT_REQUEST_BYTES }), async (c) => {
|
|
23
83
|
try {
|
|
24
84
|
const { id } = c.req.param();
|
|
25
85
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -37,7 +97,7 @@ app.post('/:id/images', async (c) => {
|
|
|
37
97
|
const arrayBuffer = await file.arrayBuffer();
|
|
38
98
|
const buffer = Buffer.from(arrayBuffer);
|
|
39
99
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
40
|
-
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Max:
|
|
100
|
+
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Max: 50 MB` }, 400);
|
|
41
101
|
}
|
|
42
102
|
const worktreePath = workspace.worktreePath;
|
|
43
103
|
const result = await imageService.saveImage(worktreePath, buffer, file.name);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
6
|
+
import { EXTERNAL_MCP_TOOLS } from '../../shared/workspace-dialogue-tools.js';
|
|
7
|
+
import { createMcpClientContext } from '../services/mcp-client-context.js';
|
|
8
|
+
import { executeWorkspaceDialogueTool } from '../services/workspace-dialogue-service.js';
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
app.use('*', bodyLimit({ maxSize: 1024 * 1024 }));
|
|
11
|
+
app.all('/', async (c) => {
|
|
12
|
+
const server = new Server({ name: 'kobo-workspaces', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
13
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
14
|
+
sessionIdGenerator: undefined,
|
|
15
|
+
enableJsonResponse: true,
|
|
16
|
+
});
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EXTERNAL_MCP_TOOLS }));
|
|
18
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
19
|
+
try {
|
|
20
|
+
const source = createMcpClientContext(c.req.header('X-Kobo-Client-Name'), c.req.header('X-Kobo-Client-Transport') === 'stdio' ? 'stdio' : 'http', c.req.header('X-Kobo-Client-Name-Encoding'));
|
|
21
|
+
const data = await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
|
|
22
|
+
const structuredContent = data && typeof data === 'object' && !Array.isArray(data) ? data : undefined;
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
25
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
26
|
+
...(structuredContent?.accepted === false ? { isError: true } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
isError: true,
|
|
32
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
return await transport.handleRequest(c.req.raw);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await server.close();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
export default app;
|
|
@@ -77,7 +77,7 @@ app.post('/diagnose', async (c) => {
|
|
|
77
77
|
if (!pr)
|
|
78
78
|
return c.json({ error: `Pull request #${prNumber} not found` }, 404);
|
|
79
79
|
const workspaces = listWorkspaces(true).filter((workspace) => path.resolve(workspace.projectPath) === path.resolve(projectPath));
|
|
80
|
-
const report = prCheckout.diagnoseLocalState(projectPath, pr.headBranch, worktreesPath ?? null, attachedWorktreePaths(workspaces), workspaces);
|
|
80
|
+
const report = await prCheckout.diagnoseLocalState(projectPath, pr.headBranch, worktreesPath ?? null, attachedWorktreePaths(workspaces), workspaces);
|
|
81
81
|
if (pr.isFork)
|
|
82
82
|
report.blockers.push({ kind: 'fork-pr' });
|
|
83
83
|
return c.json({ report, pr, fingerprint: await prCheckout.computeFingerprint(report) });
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { searchEvents } from '../services/search-service.js';
|
|
2
|
+
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
3
3
|
const DEFAULT_LIMIT = 50;
|
|
4
4
|
const MAX_LIMIT = 200;
|
|
5
5
|
const app = new Hono();
|
|
6
6
|
// GET /api/search?q=...&limit=50&includeArchived=true
|
|
7
7
|
// Search readable text across ws_events (user messages + agent outputs),
|
|
8
8
|
// joined with workspaces. Returns up to `limit` snippets, most recent first.
|
|
9
|
-
app.get('/', (c) => {
|
|
9
|
+
app.get('/status', (c) => {
|
|
10
|
+
try {
|
|
11
|
+
return c.json(getSearchIndexStatus());
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 503);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
app.get('/', async (c) => {
|
|
10
18
|
const qRaw = c.req.query('q') ?? '';
|
|
11
19
|
const q = qRaw.trim();
|
|
12
20
|
if (!q) {
|
|
@@ -22,12 +30,13 @@ app.get('/', (c) => {
|
|
|
22
30
|
}
|
|
23
31
|
const includeArchived = c.req.query('includeArchived') === 'true';
|
|
24
32
|
try {
|
|
25
|
-
const results = searchEvents(q, { limit, includeArchived });
|
|
33
|
+
const results = await searchEvents(q, { limit, includeArchived }, c.req.raw.signal);
|
|
34
|
+
c.header('X-Kobo-Search-Partial', String(getSearchIndexStatus().state !== 'ready'));
|
|
26
35
|
return c.json(results);
|
|
27
36
|
}
|
|
28
37
|
catch (err) {
|
|
29
38
|
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
-
return c.json({ error: message },
|
|
39
|
+
return c.json({ error: message }, 503);
|
|
31
40
|
}
|
|
32
41
|
});
|
|
33
42
|
export default app;
|
|
@@ -3,6 +3,7 @@ import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
|
|
|
3
3
|
import { getDb } from '../db/index.js';
|
|
4
4
|
import { getBackendPort } from '../services/agent/orchestrator.js';
|
|
5
5
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
6
|
+
import { getMcpConnectionInfo } from '../services/mcp-connection-info-service.js';
|
|
6
7
|
import { generateToken, getLanUrls } from '../services/network-access-service.js';
|
|
7
8
|
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from '../services/review-template-service.js';
|
|
8
9
|
import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaults.js';
|
|
@@ -82,6 +83,10 @@ app.get('/ws-events-retention-preview', (c) => {
|
|
|
82
83
|
return c.json({ error: message }, 500);
|
|
83
84
|
}
|
|
84
85
|
});
|
|
86
|
+
// GET /api/settings/mcp — protected connection metadata, without credentials
|
|
87
|
+
app.get('/mcp', (c) => {
|
|
88
|
+
return c.json(getMcpConnectionInfo(getBackendPort(), settingsService.getGlobalSettings(), getDb().name));
|
|
89
|
+
});
|
|
85
90
|
// GET /api/settings/network — network access state + LAN URLs
|
|
86
91
|
app.get('/network', (c) => {
|
|
87
92
|
try {
|