@loicngr/kobo 1.11.45 → 2.1.1
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 +17 -2
- package/CHANGELOG.md +12 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONFIGURATION.md +1968 -0
- package/CONTRIBUTING.md +83 -0
- package/DESIGN.md +296 -0
- package/README.md +35 -7
- package/SECURITY.md +54 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/server/db/migrations.js +31 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/index.js +6 -0
- package/dist/server/routes/environment.js +18 -0
- package/dist/server/routes/health.js +17 -2
- package/dist/server/routes/integrations.js +29 -0
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/sounds.js +60 -0
- package/dist/server/routes/usage.js +28 -2
- package/dist/server/routes/workspaces.js +46 -12
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -4
- package/dist/server/services/agent/engines/codex/options-builder.js +1 -0
- package/dist/server/services/agent/engines/codex/spawn.js +2 -2
- package/dist/server/services/agent/integration-mcp.js +49 -0
- package/dist/server/services/agent/orchestrator.js +6 -2
- package/dist/server/services/auto-loop-service.js +45 -25
- package/dist/server/services/custom-sound-service.js +0 -0
- package/dist/server/services/environment-check-service.js +125 -0
- package/dist/server/services/initial-prompt-template-service.js +8 -21
- package/dist/server/services/integration-config-service.js +65 -0
- package/dist/server/services/legacy-public-prompts.js +112 -0
- package/dist/server/services/notion-service.js +25 -61
- package/dist/server/services/sentry-service.js +17 -12
- package/dist/server/services/settings-defaults.js +5 -0
- package/dist/server/services/settings-service.js +112 -23
- package/dist/server/services/skill-suite-prompts.js +29 -22
- package/dist/server/services/templates-service.js +31 -8
- package/dist/server/services/terminal-service.js +16 -0
- package/dist/server/services/usage/reliability.js +24 -0
- package/dist/server/services/wakeup-service.js +7 -5
- package/dist/server/services/workspace-service.js +25 -4
- package/dist/server/services/worktree-service.js +3 -1
- package/dist/server/utils/git-repo-lock.js +7 -3
- package/dist/server/utils/mcp-client.js +26 -12
- package/dist/server/utils/mcp-process.js +25 -0
- package/dist/server/utils/node-pty-spawn-helper.js +31 -0
- package/dist/server/utils/paths.js +7 -0
- package/dist/shared/auto-loop-prompts.js +6 -2
- package/dist/shared/automatic-admission.js +1 -0
- package/dist/shared/environment.js +1 -0
- package/dist/shared/notification-assets.js +73 -0
- package/dist/shared/skill-suite-prompts.js +6 -3
- package/dist/shared/workflow-policy.js +25 -0
- package/docs/getting-started.md +47 -0
- package/docs/troubleshooting.md +49 -0
- package/package.json +19 -7
- package/src/client/dist/pwa/assets/ActivityFeed-DeJ0V6CW.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-CnXTnFhe.js → ChangelogPage-OoOnn4q9.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-CKylN45q.js → ComparisonPanel-BYpe6Nch.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-COQwHyqL.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CZ2036MC.js +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-CXBmMV9R.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BeCttB-x.js → DocumentsPanel-mLGU5aTb.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BJBBYeOc.js → DrawerToggleButton-C39pfWBD.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-BQk7kJO-.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-BFCnyhE2.css → GitPanel-C4wOcZV2.css} +1 -1
- package/src/client/dist/pwa/assets/{HealthPage-l88TEk-k.js → HealthPage-CnJWXfa8.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Cc4XzCAy.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-D3zfT61Y.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-44Mj-y_1.js → QBtnDropdown-DGLLlBf3.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-Bok1m8Wi.js → QExpansionItem-CQ2LzM8P.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DoTWgQAO.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-BDihn2aY.js → QTooltip-DKvUeiR_.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-DIMzNIO3.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-BwZ2TX1G.js → SearchPage-COWnMxRZ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-B9cLhFnO.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-W6N01tGz.css → SettingsPage-BYdLXJEJ.css} +1 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BJKssJyI.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-Cq-FR7A7.js → TerminalPanel-C_HNpN1X.js} +4 -4
- package/src/client/dist/pwa/assets/{TouchPan-BQ1cvzRC.js → TouchPan-CCllyaAF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Dz1yEz_z.js → TourReplayButton-Cp2UnxWa.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B-SuQuS_.js +1 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BbUvR1Vf.css → WorkspacePage--5HsnMQ1.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6lN4iF5W.js +4 -0
- package/src/client/dist/pwa/assets/{browser-BmWgBx9M.js → browser-uy3hmwKo.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-6EjBmYYY.js → build-path-tree-DZrp6Aji.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-Du8ReMlU.js +1 -0
- package/src/client/dist/pwa/assets/de-CTE3Xb7Z.js +17 -0
- package/src/client/dist/pwa/assets/{documents-BZ8bwcjr.js → documents-BtNrB6d0.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-CK7--ckY.js → editor.api-P-eJRcVK.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-ChUC4xtk.js +9 -0
- package/src/client/dist/pwa/assets/engineFeatures-D-0NPZBE.js +1 -0
- package/src/client/dist/pwa/assets/es-CZxg1xjh.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8VyjASo.js → expand-template-ByoC_kxX.js} +1 -1
- package/src/client/dist/pwa/assets/{format-LtTLLitJ.js → format-CeidAPFn.js} +7 -7
- package/src/client/dist/pwa/assets/{formatters-Bl9zA7Ai.js → formatters-BtG6lb4d.js} +1 -1
- package/src/client/dist/pwa/assets/fr-C7HCclxj.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DxezMaje.js → freemarker2-CDYXi2tK.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DK_Bj6Rx.js → handlebars-AVkJuN-_.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DFfsfStD.js → html-CpvEZlmO.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-KVkTJG-4.js +1 -0
- package/src/client/dist/pwa/assets/index-DsaWUT_Z.js +20 -0
- package/src/client/dist/pwa/assets/it-DoOWwqZz.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-DuNbcaL7.js → javascript-BYnnBLH9.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CM3JKyhv.js → liquid-CzqlB3Um.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-BG3zYwNF.js → mdx-hrYFnJVZ.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-DLscZGH-.js → monaco-contributions-D2YXqNaH.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DnhZHwXT.js → monaco.contribution-Dl22KqTL.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-Dq0I5kjv.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-vV3Rm4_v.js +1 -0
- package/src/client/dist/pwa/assets/{pending-diff-open-dYIFdS4r.js → pending-diff-open-BJYqBOUG.js} +1 -1
- package/src/client/dist/pwa/assets/permissionModes-B-ciuUbz.js +36 -0
- package/src/client/dist/pwa/assets/{project-color-R_1je4vW.js → project-color-W5mMgIKK.js} +1 -1
- package/src/client/dist/pwa/assets/{python-COs6pHk2.js → python-DafgCozG.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-Jl1VqMsk.js → razor-CWYrTLCm.js} +1 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-CE5RIL0s.js +69 -0
- package/src/client/dist/pwa/assets/{typescript-BU_TUuAM.js → typescript-DLSIuW9u.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-CDdnvIaG.js +2 -0
- package/src/client/dist/pwa/assets/workspace-templates-C88VmFdQ.js +1 -0
- package/src/client/dist/pwa/assets/{xml-gFFsobxE.js → xml-BAmdYarn.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DaXaqaGD.js → yaml-RQqNMXWh.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sounds/neutral.wav +0 -0
- package/src/client/dist/pwa/sounds/ready.wav +0 -0
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-CZQLfoV1.js +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-D8g7_r4S.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-Csf3WmLc.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +0 -2
- package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-Bg6nKBcA.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BCVOP8vp.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +0 -4
- package/src/client/dist/pwa/assets/de-COObokpu.js +0 -17
- package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +0 -1
- package/src/client/dist/pwa/assets/es-By2Q9Tca.js +0 -17
- package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
- package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +0 -17
- package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +0 -1
- package/src/client/dist/pwa/assets/index-BLd8DAzg.js +0 -20
- package/src/client/dist/pwa/assets/it-CeK5lyIA.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-BJiPbO5l.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-D4p3qPkp.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +0 -36
- package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +0 -67
- package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +0 -2
- package/src/client/dist/pwa/assets/workspace-templates-BCKR8TXr.js +0 -1
- package/src/client/dist/pwa/sounds/7eme-compagnie-03.mp3 +0 -0
- package/src/client/dist/pwa/sounds/aller-ftg.mp3 +0 -0
- package/src/client/dist/pwa/sounds/arrete-de-mentir.mp3 +0 -0
- package/src/client/dist/pwa/sounds/arretez-les-messages.mp3 +0 -0
- package/src/client/dist/pwa/sounds/bah-alors-on-est-nul.mp3 +0 -0
- package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ca_va_peter.mp3 +0 -0
- package/src/client/dist/pwa/sounds/dry-fart.mp3 +0 -0
- package/src/client/dist/pwa/sounds/faaah.mp3 +0 -0
- package/src/client/dist/pwa/sounds/for-shure.mp3 +0 -0
- package/src/client/dist/pwa/sounds/gta-v-death.mp3 +0 -0
- package/src/client/dist/pwa/sounds/hey.mp3 +0 -0
- package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
- package/src/client/dist/pwa/sounds/nan-tu-degages.mp3 +0 -0
- package/src/client/dist/pwa/sounds/nan-wallah-pardon.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ouais-cest-greg.mp3 +0 -0
- package/src/client/dist/pwa/sounds/pas-ca-zinedine.mp3 +0 -0
- package/src/client/dist/pwa/sounds/ta-gueule.mp3 +0 -0
- package/src/client/dist/pwa/sounds/travail_termine.mp3 +0 -0
- package/src/client/dist/pwa/sounds/tu-vas-la-fermer.mp3 +0 -0
- package/src/client/dist/pwa/sounds/warcraft-3-humain-travail.mp3 +0 -0
- package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { getEnvironmentReport } from '../services/environment-check-service.js';
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
app.get('/', async (c) => {
|
|
5
|
+
const engine = c.req.query('engine') ?? 'claude-code';
|
|
6
|
+
const projectPath = c.req.query('projectPath')?.trim();
|
|
7
|
+
if (!['claude-code', 'codex'].includes(engine) ||
|
|
8
|
+
(projectPath && (projectPath.length > 4096 || projectPath.includes('\0')))) {
|
|
9
|
+
return c.json({ error: 'Invalid environment check request' }, 400);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return c.json(await getEnvironmentReport({ engine: engine, projectPath }));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return c.json({ error: 'Environment check unavailable; retry shortly' }, 503);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
export default app;
|
|
@@ -5,6 +5,7 @@ import { Hono } from 'hono';
|
|
|
5
5
|
import { getDb } from '../db/index.js';
|
|
6
6
|
import { SCHEMA_VERSION } from '../db/migrations.js';
|
|
7
7
|
import { resolveCodexBinary } from '../services/agent/engines/codex/spawn.js';
|
|
8
|
+
import { hasIntegrationMcpConfig } from '../services/agent/integration-mcp.js';
|
|
8
9
|
import { getGlobalSettings, getProjectSettings, SETTINGS_SCHEMA_VERSION } from '../services/settings-service.js';
|
|
9
10
|
import { readRecentLogs } from '../utils/logger.js';
|
|
10
11
|
import { getDbPath, getKoboHome, getPackageVersion } from '../utils/paths.js';
|
|
@@ -12,6 +13,14 @@ import { slugifyProjectName } from '../utils/project-slug.js';
|
|
|
12
13
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
13
14
|
const app = new Hono();
|
|
14
15
|
const execFileAsync = promisify(execFileCb);
|
|
16
|
+
function integrationConfigured(integration, key) {
|
|
17
|
+
try {
|
|
18
|
+
return hasIntegrationMcpConfig(integration, key);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
15
24
|
async function checkCli(binary) {
|
|
16
25
|
try {
|
|
17
26
|
const { stdout } = await execFileAsync(binary, ['--version'], { encoding: 'utf-8', timeout: 2_000 });
|
|
@@ -182,8 +191,14 @@ app.get('/report', async (c) => {
|
|
|
182
191
|
},
|
|
183
192
|
agentSessions: { orphaned },
|
|
184
193
|
integrations: {
|
|
185
|
-
notion: {
|
|
186
|
-
|
|
194
|
+
notion: {
|
|
195
|
+
configured: integrationConfigured('notion', healthGlobalSettings.notionMcpKey),
|
|
196
|
+
enabled: healthGlobalSettings.notionEnabled,
|
|
197
|
+
},
|
|
198
|
+
sentry: {
|
|
199
|
+
configured: integrationConfigured('sentry', healthGlobalSettings.sentryMcpKey),
|
|
200
|
+
enabled: healthGlobalSettings.sentryEnabled,
|
|
201
|
+
},
|
|
187
202
|
editor: { configured: Boolean(healthGlobalSettings.editorCommand) },
|
|
188
203
|
},
|
|
189
204
|
active: {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import { getIntegrationStatus, saveIntegrationConfig, } from '../services/integration-config-service.js';
|
|
4
|
+
const app = new Hono();
|
|
5
|
+
app.use('*', bodyLimit({ maxSize: 64 * 1024 }));
|
|
6
|
+
app.use('/:integration', async (c, next) => {
|
|
7
|
+
if (!['notion', 'sentry'].includes(c.req.param('integration') ?? ''))
|
|
8
|
+
return c.json({ error: 'Unknown integration' }, 404);
|
|
9
|
+
await next();
|
|
10
|
+
});
|
|
11
|
+
app.get('/:integration', (c) => {
|
|
12
|
+
try {
|
|
13
|
+
return c.json(getIntegrationStatus(c.req.param('integration')));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return c.json({ error: 'Integration configuration is unavailable' }, 503);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
app.put('/:integration', async (c) => {
|
|
20
|
+
try {
|
|
21
|
+
const value = await c.req.json();
|
|
22
|
+
saveIntegrationConfig(c.req.param('integration'), value);
|
|
23
|
+
return c.json(getIntegrationStatus(c.req.param('integration')));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return c.json({ error: 'Cannot save integration configuration; check the connection fields and local storage' }, 400);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export default app;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
|
|
3
|
+
import { isWorkflowPolicy } from '../../shared/workflow-policy.js';
|
|
3
4
|
import { getDb } from '../db/index.js';
|
|
4
5
|
import { getBackendPort } from '../services/agent/orchestrator.js';
|
|
5
6
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
@@ -163,6 +164,8 @@ app.put('/global', async (c) => {
|
|
|
163
164
|
// must answer 400 rather than do that.
|
|
164
165
|
if (!body)
|
|
165
166
|
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
167
|
+
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
168
|
+
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
166
169
|
const updated = settingsService.updateGlobalSettings(body);
|
|
167
170
|
// The client assigns this response straight into its store, so echoing the
|
|
168
171
|
// real credentials back would undo the masking on GET the first time
|
|
@@ -211,6 +214,8 @@ app.put('/projects/:encodedPath', async (c) => {
|
|
|
211
214
|
// `{}` CREATES a project entry with defaults; a malformed body must not.
|
|
212
215
|
if (!body)
|
|
213
216
|
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
217
|
+
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
218
|
+
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
214
219
|
const project = settingsService.upsertProject(projectPath, body);
|
|
215
220
|
return c.json(project);
|
|
216
221
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
4
|
+
import { MAX_CUSTOM_SOUND_BYTES } from '../../shared/notification-assets.js';
|
|
5
|
+
import { CustomSoundError, deleteCustomSound, listCustomSounds, readCustomSound, saveCustomSound, } from '../services/custom-sound-service.js';
|
|
6
|
+
/** Multipart framing and the field name add a small, bounded overhead. */
|
|
7
|
+
const MAX_REQUEST_BYTES = MAX_CUSTOM_SOUND_BYTES + 64 * 1024;
|
|
8
|
+
const app = new Hono();
|
|
9
|
+
// `listCustomSounds` degrades a damaged manifest to an empty list; it never throws.
|
|
10
|
+
app.get('/', (c) => c.json({ sounds: listCustomSounds() }));
|
|
11
|
+
app.post('/', bodyLimit({ maxSize: MAX_REQUEST_BYTES }), async (c) => {
|
|
12
|
+
let form;
|
|
13
|
+
try {
|
|
14
|
+
form = await c.req.raw.formData();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return c.json({ error: 'Invalid multipart body' }, 400);
|
|
18
|
+
}
|
|
19
|
+
const files = form.getAll('sound');
|
|
20
|
+
if (files.length !== 1 || !(files[0] instanceof File))
|
|
21
|
+
return c.json({ error: 'Expected one sound file' }, 400);
|
|
22
|
+
try {
|
|
23
|
+
return c.json(await saveCustomSound(files[0]), 201);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Only the stable code crosses the boundary: a filesystem error may carry
|
|
27
|
+
// the layout of the user's home directory.
|
|
28
|
+
if (error instanceof CustomSoundError)
|
|
29
|
+
return c.json({ error: 'Invalid sound file', code: error.code }, 400);
|
|
30
|
+
return c.json({ error: 'Cannot store the sound file' }, 500);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
app.delete('/:id', (c) => {
|
|
34
|
+
try {
|
|
35
|
+
return deleteCustomSound(c.req.param('id')) ? c.body(null, 204) : c.json({ error: 'Sound not found' }, 404);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return c.json({ error: 'Cannot remove the sound file' }, 500);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// An unknown or deleted sound answers 404; the client then falls back to a bundled tone.
|
|
42
|
+
app.get('/:id/file', async (c) => {
|
|
43
|
+
const sound = readCustomSound(c.req.param('id'));
|
|
44
|
+
if (!sound)
|
|
45
|
+
return c.json({ error: 'Sound not found' }, 404);
|
|
46
|
+
let content;
|
|
47
|
+
try {
|
|
48
|
+
content = await fs.promises.readFile(sound.filePath);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// The file vanished between the manifest lookup and the read.
|
|
52
|
+
return c.json({ error: 'Sound not found' }, 404);
|
|
53
|
+
}
|
|
54
|
+
c.header('Content-Type', sound.contentType);
|
|
55
|
+
// Content-addressed by a generated id: the bytes behind an id never change.
|
|
56
|
+
c.header('Cache-Control', 'private, max-age=3600, immutable');
|
|
57
|
+
c.header('X-Content-Type-Options', 'nosniff');
|
|
58
|
+
return c.body(new Uint8Array(content));
|
|
59
|
+
});
|
|
60
|
+
export default app;
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { getDb } from '../db/index.js';
|
|
3
3
|
import { refreshNow } from '../services/usage/poller.js';
|
|
4
|
-
import { computeEngineReliability } from '../services/usage/reliability.js';
|
|
4
|
+
import { clearReliabilityReset, computeEngineReliability, getReliabilityResetAt, resetReliability, } from '../services/usage/reliability.js';
|
|
5
5
|
const app = new Hono();
|
|
6
6
|
// GET /api/usage/reliability — how each engine and model actually behaved.
|
|
7
7
|
app.get('/reliability', (c) => {
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
const db = getDb();
|
|
10
|
+
return c.json({ engines: computeEngineReliability(db), resetAt: getReliabilityResetAt(db) });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
return c.json({ error: message }, 500);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
// POST /api/usage/reliability/reset - count again from now. Sessions are kept.
|
|
18
|
+
// Declared before `/:providerId/refresh` so the static path is never captured.
|
|
19
|
+
app.post('/reliability/reset', (c) => {
|
|
20
|
+
try {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
const resetAt = resetReliability(db);
|
|
23
|
+
return c.json({ engines: computeEngineReliability(db), resetAt });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
return c.json({ error: message }, 500);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// DELETE /api/usage/reliability/reset - count the whole history again.
|
|
31
|
+
app.delete('/reliability/reset', (c) => {
|
|
32
|
+
try {
|
|
33
|
+
const db = getDb();
|
|
34
|
+
clearReliabilityReset(db);
|
|
35
|
+
return c.json({ engines: computeEngineReliability(db), resetAt: null });
|
|
10
36
|
}
|
|
11
37
|
catch (err) {
|
|
12
38
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
+
import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
|
|
4
5
|
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
5
6
|
import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
|
|
6
7
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
7
8
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
8
|
-
import { workspaceLifecycleReason } from '../utils/workspace-lifecycle-guard.js';
|
|
9
|
+
import { isWorkspaceLifecycleBusy, workspaceLifecycleReason } from '../utils/workspace-lifecycle-guard.js';
|
|
9
10
|
import autoLoopMessagesRoutes from './auto-loop-messages.js';
|
|
10
11
|
const execFileAsync = promisify(execFileCb);
|
|
11
12
|
import fs from 'node:fs';
|
|
@@ -62,6 +63,15 @@ import { WorkspaceLifecycleBusyError, withWorkspaceLifecycleGuard } from '../uti
|
|
|
62
63
|
import { resolveExtractedName } from '../utils/workspace-name.js';
|
|
63
64
|
import { resolveSiblingWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
64
65
|
/** Lifecycle conflicts can be retried once the current owner has stopped. */
|
|
66
|
+
/** Symlink-resolved path, or the lexically resolved one when it does not exist. */
|
|
67
|
+
function realPathOrResolved(p) {
|
|
68
|
+
try {
|
|
69
|
+
return fs.realpathSync(p);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return path.resolve(p);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
65
75
|
function workspaceErrorStatus(err) {
|
|
66
76
|
return err instanceof AgentStopError || err instanceof WorkspaceLifecycleBusyError ? 409 : 500;
|
|
67
77
|
}
|
|
@@ -452,6 +462,8 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
452
462
|
let setupScriptConfigured = false;
|
|
453
463
|
try {
|
|
454
464
|
const { body, attachments } = await readWorkspaceCreationRequest(c.req.raw);
|
|
465
|
+
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
466
|
+
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
455
467
|
// workingBranch is derived from git when worktreePath is provided, so
|
|
456
468
|
// it's not required in that flow. The other 3 fields stay mandatory.
|
|
457
469
|
if (!body.name || !body.projectPath || !body.sourceBranch) {
|
|
@@ -552,11 +564,10 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
552
564
|
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
553
565
|
}
|
|
554
566
|
try {
|
|
555
|
-
const commonDir = execFileSync('git', ['-C', body.worktreePath, 'rev-parse', '--git-common-dir'], {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (path.resolve(commonDir) !== path.resolve(expectedCommonDir)) {
|
|
567
|
+
const commonDir = execFileSync('git', ['-C', body.worktreePath, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf-8' }).trim();
|
|
568
|
+
// Git reports the common dir symlink-resolved while the project path may be
|
|
569
|
+
// reached through a link (macOS temp dirs: /var -> /private/var).
|
|
570
|
+
if (realPathOrResolved(commonDir) !== realPathOrResolved(path.join(body.projectPath, '.git'))) {
|
|
560
571
|
const failure = `Worktree '${body.worktreePath}' belongs to a different repository`;
|
|
561
572
|
emitCreateFailed(creationId, 'inspect-worktree', failure);
|
|
562
573
|
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
@@ -794,6 +805,7 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
794
805
|
model: body.model,
|
|
795
806
|
brainstormModel: body.brainstormModel,
|
|
796
807
|
reasoningEffort: body.reasoningEffort,
|
|
808
|
+
workflowPolicy: resolveWorkflowPolicy(globalSettings.workflowPolicy, projectSettings?.workflowPolicy, body.workflowPolicy),
|
|
797
809
|
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings, body.engine ?? 'claude-code'),
|
|
798
810
|
engine: body.engine,
|
|
799
811
|
// Set by the engine-comparison flow, which posts here once per engine
|
|
@@ -1109,9 +1121,9 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
1109
1121
|
`## Additional Context\n${extra}\n\n` +
|
|
1110
1122
|
`## MCP Tools for deeper analysis\n` +
|
|
1111
1123
|
`If you need more context, the following Sentry MCP tools are available:\n` +
|
|
1112
|
-
`- \`
|
|
1113
|
-
`- \`
|
|
1114
|
-
`- \`
|
|
1124
|
+
`- \`get_sentry_resource(url, resourceType) [use the Kōbō-managed Sentry namespace supplied for this session]\` — fetch the issue, breadcrumbs, replay, or trace\n` +
|
|
1125
|
+
`- \`search_issue_events(organizationSlug, issueId='${sentryContent.issueId}')\` — recent events for this issue\n` +
|
|
1126
|
+
`- \`get_issue_tag_values(organizationSlug, issueId='${sentryContent.issueId}', key)\` — filter by tag (environment, user, browser, …)\n`;
|
|
1115
1127
|
fs.writeFileSync(sentryFilePath, md, 'utf-8');
|
|
1116
1128
|
workspaceService.createTask(workspace.id, {
|
|
1117
1129
|
title: `Fix: ${sentryContent.title || sentryContent.issueId || `Sentry #${sentryContent.issueNumericId}`}`,
|
|
@@ -1232,9 +1244,9 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
1232
1244
|
`5. Confirm the test passes, run related tests\n` +
|
|
1233
1245
|
`6. Commit referencing the Sentry Short-ID (e.g. "fix(scope): description (${sentryContent.issueId})") — Sentry auto-closes the issue when the commit is merged\n` +
|
|
1234
1246
|
`\nIf you need more context, Sentry MCP tools are available:\n` +
|
|
1235
|
-
`-
|
|
1236
|
-
`-
|
|
1237
|
-
`-
|
|
1247
|
+
`- get_sentry_resource(url, resourceType) [use the Kōbō-managed Sentry namespace supplied for this session] — fetch the issue, breadcrumbs, replay or trace\n` +
|
|
1248
|
+
`- search_issue_events(organizationSlug, issueId='${sentryContent.issueId}') — recent events\n` +
|
|
1249
|
+
`- get_issue_tag_values(organizationSlug, issueId='${sentryContent.issueId}', key) — filter by tag\n`;
|
|
1238
1250
|
if (sentryTpl.trim().length > 0) {
|
|
1239
1251
|
const renderedSentry = renderSentryInitialPrompt(sentryTpl, {
|
|
1240
1252
|
issueId: sentryContent.issueId,
|
|
@@ -1680,6 +1692,13 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1680
1692
|
});
|
|
1681
1693
|
app.route('/', autoLoopMessagesRoutes);
|
|
1682
1694
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1695
|
+
// Admission inspection never changes deadlines, pending schedules or lifecycle state.
|
|
1696
|
+
app.get('/:id/automatic-admission', (c) => {
|
|
1697
|
+
const id = c.req.param('id');
|
|
1698
|
+
if (!workspaceService.getWorkspace(id))
|
|
1699
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
1700
|
+
return c.json(autoLoopService.getAutomaticAdmissionStatus(id));
|
|
1701
|
+
});
|
|
1683
1702
|
app.get('/:id/auto-loop', (c) => {
|
|
1684
1703
|
try {
|
|
1685
1704
|
const id = c.req.param('id');
|
|
@@ -2708,6 +2727,20 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
2708
2727
|
if ('agent_description' in body) {
|
|
2709
2728
|
return c.json({ error: 'agent_description must be set via the agent MCP tool, not via the API' }, 400);
|
|
2710
2729
|
}
|
|
2730
|
+
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
2731
|
+
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
2732
|
+
if (body.workflowPolicy !== undefined) {
|
|
2733
|
+
const policy = resolveWorkflowPolicy(workspace.workflowPolicy, body.workflowPolicy);
|
|
2734
|
+
const changed = WORKFLOW_ACTIONS.some((action) => policy[action] !== workspace.workflowPolicy[action]);
|
|
2735
|
+
// A live engine already received its policy. Persisting a different one cannot
|
|
2736
|
+
// revoke that prompt; wait for confirmed closure before accepting the change.
|
|
2737
|
+
if (changed && (agentManager.hasController(id) || isWorkspaceLifecycleBusy(id))) {
|
|
2738
|
+
return c.json({
|
|
2739
|
+
error: 'Stop the agent and wait for workspace operations to finish before changing workflow preferences.',
|
|
2740
|
+
code: 'workspace-busy',
|
|
2741
|
+
}, 409);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2711
2744
|
if (body.model !== undefined && (typeof body.model !== 'string' || !body.model.trim())) {
|
|
2712
2745
|
return c.json({ error: 'model must be a non-empty string' }, 400);
|
|
2713
2746
|
}
|
|
@@ -2749,6 +2782,7 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
2749
2782
|
}
|
|
2750
2783
|
}
|
|
2751
2784
|
if (body.status === undefined &&
|
|
2785
|
+
body.workflowPolicy === undefined &&
|
|
2752
2786
|
body.model === undefined &&
|
|
2753
2787
|
body.reasoningEffort === undefined &&
|
|
2754
2788
|
body.agentPermissionMode === undefined &&
|
|
@@ -43,6 +43,11 @@ export const COMPACTION_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
|
43
43
|
// `inputStream.close()`/the result-drain watchdog forever. Generous window:
|
|
44
44
|
// legitimate subagents can run for several minutes.
|
|
45
45
|
export const SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
46
|
+
/** After the last background subagent completes, the CLI normally resumes the
|
|
47
|
+
* parent on its own. Closing the input before that continuation starts makes
|
|
48
|
+
* every permission request of it fail with "Stream closed", so wait this long
|
|
49
|
+
* for a parent message before draining a stream that did not continue. */
|
|
50
|
+
export const BACKGROUND_CONTINUATION_GRACE_MS = 60_000;
|
|
46
51
|
const MAX_PENDING_USER_MESSAGES = 20;
|
|
47
52
|
function toMcpServersMap(specs) {
|
|
48
53
|
if (!specs || specs.length === 0)
|
|
@@ -406,6 +411,30 @@ export function createClaudeCodeEngine() {
|
|
|
406
411
|
clearTimeout(resultDrainTimer);
|
|
407
412
|
resultDrainTimer = undefined;
|
|
408
413
|
};
|
|
414
|
+
// Drain for a stream whose last background subagent completed without
|
|
415
|
+
// a parent continuation. Any parent message cancels it: that
|
|
416
|
+
// continuation's own `result` then closes the input.
|
|
417
|
+
let continuationGraceTimer;
|
|
418
|
+
const clearContinuationGrace = () => {
|
|
419
|
+
if (!continuationGraceTimer)
|
|
420
|
+
return;
|
|
421
|
+
clearTimeout(continuationGraceTimer);
|
|
422
|
+
continuationGraceTimer = undefined;
|
|
423
|
+
};
|
|
424
|
+
const armContinuationGrace = () => {
|
|
425
|
+
clearContinuationGrace();
|
|
426
|
+
continuationGraceTimer = setTimeout(() => {
|
|
427
|
+
continuationGraceTimer = undefined;
|
|
428
|
+
if (activeSubagentTaskIds.size > 0 ||
|
|
429
|
+
pendingResolvers.size > 0 ||
|
|
430
|
+
pendingToolCallIds.size > 0 ||
|
|
431
|
+
inputStream.hasUnansweredInput(completedResponses))
|
|
432
|
+
return;
|
|
433
|
+
inputStream.close();
|
|
434
|
+
armResultDrainWatchdog();
|
|
435
|
+
}, BACKGROUND_CONTINUATION_GRACE_MS);
|
|
436
|
+
continuationGraceTimer.unref?.();
|
|
437
|
+
};
|
|
409
438
|
// Safety net for the "waiting on a background subagent" branch below:
|
|
410
439
|
// if `activeSubagentToolCallIds` never empties (a missed/unrecognised
|
|
411
440
|
// terminal notification), the workspace would otherwise stay `executing`
|
|
@@ -447,6 +476,7 @@ export function createClaudeCodeEngine() {
|
|
|
447
476
|
// progress notifications alone do not end the between-turn wait.
|
|
448
477
|
if (msg.type === 'assistant' || msg.type === 'user' || msg.type === 'stream_event') {
|
|
449
478
|
waitingForBackground = false;
|
|
479
|
+
clearContinuationGrace();
|
|
450
480
|
}
|
|
451
481
|
// This SDK message proves that any drain condition armed by a
|
|
452
482
|
// *previous* message is no longer current. A drain armed while
|
|
@@ -501,14 +531,15 @@ export function createClaudeCodeEngine() {
|
|
|
501
531
|
else if (ev.kind === 'tool:result')
|
|
502
532
|
pendingToolCallIds.delete(ev.toolCallId);
|
|
503
533
|
}
|
|
504
|
-
// After a settled result, the last background completion
|
|
505
|
-
//
|
|
534
|
+
// After a settled result, the last background completion drains
|
|
535
|
+
// the stream only if the parent does not continue (see
|
|
536
|
+
// BACKGROUND_CONTINUATION_GRACE_MS): its continuation still needs
|
|
537
|
+
// the input for permission requests.
|
|
506
538
|
if (subagentStallTimer &&
|
|
507
539
|
activeSubagentTaskIds.size === 0 &&
|
|
508
540
|
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
509
541
|
clearSubagentStallWatchdog();
|
|
510
|
-
|
|
511
|
-
armResultDrainWatchdog();
|
|
542
|
+
armContinuationGrace();
|
|
512
543
|
}
|
|
513
544
|
for (const ev of events) {
|
|
514
545
|
if (ev.kind === 'session:started')
|
|
@@ -527,6 +558,7 @@ export function createClaudeCodeEngine() {
|
|
|
527
558
|
reevaluateLivenessPause();
|
|
528
559
|
if (msg.type === 'result') {
|
|
529
560
|
waitingForBackground = false;
|
|
561
|
+
clearContinuationGrace();
|
|
530
562
|
pendingToolCallIds.clear();
|
|
531
563
|
completedResponses++;
|
|
532
564
|
emitTurnCompletedIfSettled();
|
|
@@ -581,6 +613,7 @@ export function createClaudeCodeEngine() {
|
|
|
581
613
|
// iterator has exited — clear it so a healthy run never triggers a
|
|
582
614
|
// stray abort after it already ended.
|
|
583
615
|
clearResultDrainWatchdog();
|
|
616
|
+
clearContinuationGrace();
|
|
584
617
|
turnLiveness.stop();
|
|
585
618
|
clearSubagentStallWatchdog();
|
|
586
619
|
clearCompactionStallTimer();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
const requireFn = createRequire(import.meta.url);
|
|
4
5
|
export function resolveCodexBinary() {
|
|
5
6
|
try {
|
|
@@ -7,8 +8,7 @@ export function resolveCodexBinary() {
|
|
|
7
8
|
const pkg = requireFn(pkgPath);
|
|
8
9
|
const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.codex;
|
|
9
10
|
if (binRel) {
|
|
10
|
-
|
|
11
|
-
return url.pathname;
|
|
11
|
+
return path.resolve(path.dirname(pkgPath), binRel);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
catch {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { readClaudeMcpEntry } from '../../utils/mcp-client.js';
|
|
3
|
+
import { getIntegrationConfig } from '../integration-config-service.js';
|
|
4
|
+
import { buildNotionMcpConfig } from '../notion-service.js';
|
|
5
|
+
import { readSentryMcpConfig } from '../sentry-service.js';
|
|
6
|
+
/** Availability only: no provider process or credential-bearing response. */
|
|
7
|
+
export function hasIntegrationMcpConfig(integration, selectedKey) {
|
|
8
|
+
if (getIntegrationConfig(integration))
|
|
9
|
+
return true;
|
|
10
|
+
if (integration === 'notion' &&
|
|
11
|
+
(process.env.NOTION_API_TOKEN || process.env.NOTION_TOKEN || process.env.OPENAPI_MCP_HEADERS))
|
|
12
|
+
return true;
|
|
13
|
+
const key = selectedKey?.trim();
|
|
14
|
+
return Boolean(readClaudeMcpEntry((name) => key ? name === key : integration === 'notion' ? name === 'notion' : /sentry/i.test(name)));
|
|
15
|
+
}
|
|
16
|
+
/** Resolve at every launch/resume; never write credentials into a project file. */
|
|
17
|
+
export function buildIntegrationMcpServers(settings, onUnavailable) {
|
|
18
|
+
const servers = [];
|
|
19
|
+
// Fresh names avoid Codex recursively merging stale native config fields.
|
|
20
|
+
const launch = nanoid(12);
|
|
21
|
+
for (const integration of ['notion', 'sentry']) {
|
|
22
|
+
if (!settings[`${integration}Enabled`])
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
const key = settings[`${integration}McpKey`];
|
|
26
|
+
if (!hasIntegrationMcpConfig(integration, key))
|
|
27
|
+
continue;
|
|
28
|
+
const config = integration === 'notion' ? buildNotionMcpConfig(key, false) : readSentryMcpConfig(key, false);
|
|
29
|
+
servers.push({ name: `kobo-${integration}-${launch}`, ...config });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Optional connections cannot block ordinary missions. Never expose raw
|
|
33
|
+
// config/parse errors, which may contain credential material.
|
|
34
|
+
onUnavailable?.(integration);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return servers;
|
|
38
|
+
}
|
|
39
|
+
export function integrationMcpPrompt(servers, unavailable = []) {
|
|
40
|
+
if (!servers.length && !unavailable.length)
|
|
41
|
+
return '';
|
|
42
|
+
return [
|
|
43
|
+
'[Kōbō managed integrations]',
|
|
44
|
+
...unavailable.map((name) => `${name}: Kōbō could not load this connection. Ask the user to check Settings if needed; do not silently substitute personal credentials.`),
|
|
45
|
+
'For these integrations, use the MCP servers listed below, configured in Kōbō Settings. Prefer them over personal/native servers and namespaces from earlier turns. Discover their available tools; do not assume all servers implement the same tools. If unavailable, report the connection problem instead of silently switching credentials.',
|
|
46
|
+
...servers.map((server) => `${server.name.startsWith('kobo-notion-') ? 'Notion' : 'Sentry'}: ${server.name}`),
|
|
47
|
+
'These connections do not grant permission to modify remote data. Follow the user request and engine permissions.',
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { nanoid } from 'nanoid';
|
|
3
|
+
import { renderWorkflowPolicy } from '../../../shared/workflow-policy.js';
|
|
3
4
|
import { getDb } from '../../db/index.js';
|
|
4
5
|
import { assertAgentStopped } from '../../utils/agent-stop-result.js';
|
|
5
6
|
import { ensureKoboHome, getCompiledMcpServerPath, getDbPath, getKoboHome, getMcpServerSourcePath, getSettingsPath, getSkillsPath, } from '../../utils/paths.js';
|
|
@@ -23,6 +24,7 @@ import { getWorkspace as getWs, markWorkspaceUnread, updateWorkspaceStatus, } fr
|
|
|
23
24
|
import { resolveEngine } from './engines/registry.js';
|
|
24
25
|
import { AGENT_NO_LONGER_RUNNING_TEXT, } from './engines/types.js';
|
|
25
26
|
import { routeEvent } from './event-router.js';
|
|
27
|
+
import { buildIntegrationMcpServers, integrationMcpPrompt } from './integration-mcp.js';
|
|
26
28
|
import { SessionController } from './session-controller.js';
|
|
27
29
|
export class InterruptAgentError extends Error {
|
|
28
30
|
code;
|
|
@@ -1371,10 +1373,12 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1371
1373
|
activateSession(workspaceId, agentSessionId);
|
|
1372
1374
|
captureTaskProgressBaseline(workspaceId, agentSessionId);
|
|
1373
1375
|
const settings = ws ? readEffectiveSettingsSafe(ws.projectPath) : readEffectiveSettingsSafe(workingDir);
|
|
1376
|
+
const unavailableIntegrations = [];
|
|
1377
|
+
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), (name) => unavailableIntegrations.push(name));
|
|
1374
1378
|
const options = {
|
|
1375
1379
|
workspaceId,
|
|
1376
1380
|
workingDir,
|
|
1377
|
-
prompt: buildAgentPrompt(
|
|
1381
|
+
prompt: buildAgentPrompt(`${ws ? renderWorkflowPolicy(ws.workflowPolicy) : ''}\n${integrationMcpPrompt(integrationServers, unavailableIntegrations)}\n${prompt}`, ws?.projectPath),
|
|
1378
1382
|
model,
|
|
1379
1383
|
effort: reasoningEffort,
|
|
1380
1384
|
// Cascade: explicit caller override → workspace setting → 'bypass'.
|
|
@@ -1390,7 +1394,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1390
1394
|
}
|
|
1391
1395
|
})(),
|
|
1392
1396
|
settings,
|
|
1393
|
-
mcpServers: buildMcpServers(workspaceId, launch?.mcpEnv),
|
|
1397
|
+
mcpServers: [...buildMcpServers(workspaceId, launch?.mcpEnv), ...integrationServers],
|
|
1394
1398
|
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
1395
1399
|
};
|
|
1396
1400
|
beforeDispatch?.();
|
|
@@ -83,22 +83,41 @@ export function block(workspaceId, reason) {
|
|
|
83
83
|
quotaBackoffService.cancel(workspaceId, 'completed');
|
|
84
84
|
setRuntime(workspaceId, { state: 'blocked', reason });
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
87
|
-
export function
|
|
86
|
+
/** Read-only explanation of the shared admission gate; does not consume or reschedule work. */
|
|
87
|
+
export function getAutomaticAdmissionStatus(workspaceId) {
|
|
88
88
|
const row = getRow(workspaceId);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
const running = orchestrator.runningAgentCount();
|
|
90
|
+
const configuredLimit = settingsService.getGlobalSettings().maxConcurrentAgents;
|
|
91
|
+
const limit = typeof configuredLimit === 'number' && configuredLimit > 0 ? configuredLimit : 0;
|
|
92
|
+
let reason = null;
|
|
93
|
+
if (!row)
|
|
94
|
+
reason = 'not-found';
|
|
95
|
+
else if (row.archived_at)
|
|
96
|
+
reason = 'archived';
|
|
97
|
+
else if (row.worktree_purged_at)
|
|
98
|
+
reason = 'purged';
|
|
99
|
+
else if (orchestrator.isShuttingDown())
|
|
100
|
+
reason = 'shutdown';
|
|
101
|
+
else if (isWorkspaceLifecycleBusy(workspaceId))
|
|
102
|
+
reason = 'lifecycle';
|
|
103
|
+
else if (orchestrator.hasController(workspaceId))
|
|
104
|
+
reason = 'active-session';
|
|
105
|
+
else if (row.status === 'awaiting-user')
|
|
106
|
+
reason = 'awaiting-user';
|
|
107
|
+
else if (row.status === 'compacting')
|
|
108
|
+
reason = 'compacting';
|
|
109
|
+
// Manual schedules can resume an expired quota; active loops remain owned by quota recovery.
|
|
110
|
+
else if ((row.status === 'quota' && row.auto_loop === 1) || quotaBackoffService.getPending(workspaceId))
|
|
111
|
+
reason = 'quota';
|
|
112
|
+
else if (getRuntime(workspaceId).state === 'blocked')
|
|
113
|
+
reason = 'blocked';
|
|
114
|
+
else if (limit > 0 && running >= limit)
|
|
115
|
+
reason = 'capacity';
|
|
116
|
+
return { allowed: reason === null, reason, running, limit };
|
|
117
|
+
}
|
|
118
|
+
/** Shared admission gate for all unattended starts. Manual launches remain exempt. */
|
|
119
|
+
export function canStartAutomatically(workspaceId) {
|
|
120
|
+
return getAutomaticAdmissionStatus(workspaceId).allowed;
|
|
102
121
|
}
|
|
103
122
|
/**
|
|
104
123
|
* Enable auto-loop for the workspace. Spawns immediately if idle + pending
|
|
@@ -375,9 +394,9 @@ function ensureFinalVerification(workspaceId) {
|
|
|
375
394
|
* `orchestrator.startAgent` throws during engine.start — caught below.
|
|
376
395
|
*/
|
|
377
396
|
/**
|
|
378
|
-
* Whether another unattended session may start right now.
|
|
379
|
-
*
|
|
380
|
-
*
|
|
397
|
+
* Whether another unattended session may start right now. Fresh installs use
|
|
398
|
+
* a limit of 2; an explicit 0 means unlimited. Auto-loop, cron and wakeup
|
|
399
|
+
* launches share admission, while manual launches remain exempt.
|
|
381
400
|
*/
|
|
382
401
|
function hasFreeAgentSlot() {
|
|
383
402
|
const limit = settingsService.getGlobalSettings().maxConcurrentAgents;
|
|
@@ -501,13 +520,14 @@ ${prompt}`;
|
|
|
501
520
|
: undefined;
|
|
502
521
|
const worktreePath = row.worktree_path ??
|
|
503
522
|
resolveWorkspaceWorktreePath(row.project_path, row.working_branch, globalSettings.worktreesPath, projectSlug);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
523
|
+
const storedMode = row.agent_permission_mode;
|
|
524
|
+
const agentPermissionMode = storedMode === 'bypass' || storedMode === 'strict' || storedMode === 'interactive' ? storedMode : 'plan';
|
|
525
|
+
if (!grooming && agentPermissionMode === 'plan') {
|
|
526
|
+
const message = 'Execution requires an explicit permission choice. Change this workspace from Plan to an execution mode (strict, interactive where supported, or bypass), then resume auto-loop. Permissions were not changed.';
|
|
527
|
+
block(workspaceId, message);
|
|
528
|
+
if (opts.throwOnStartAgentError)
|
|
529
|
+
throw new Error(message);
|
|
530
|
+
return;
|
|
511
531
|
}
|
|
512
532
|
// Pre-check: if the worktree directory is gone (user `rm -rf`-ed it),
|
|
513
533
|
// fail loudly rather than letting startAgent throw a deep engine error.
|