@loicngr/kobo 1.11.8 → 1.11.10
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 +2 -1
- package/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/dist/server/db/migrations.js +155 -19
- package/dist/server/db/schema.js +93 -0
- package/dist/server/index.js +35 -26
- package/dist/server/routes/dev-server.js +10 -8
- package/dist/server/routes/documents.js +33 -3
- package/dist/server/routes/health.js +16 -15
- package/dist/server/routes/images.js +17 -2
- package/dist/server/routes/workspaces.js +64 -49
- package/dist/server/services/agent/engines/codex/engine.js +5 -1
- package/dist/server/services/agent/orchestrator.js +8 -1
- package/dist/server/services/auto-loop-service.js +1 -1
- package/dist/server/services/change-source-branch-service.js +43 -3
- package/dist/server/services/dev-server-service.js +55 -41
- package/dist/server/services/forge/bitbucket/provider.js +6 -11
- package/dist/server/services/forge/github/provider.js +10 -9
- package/dist/server/services/forge/gitlab/provider.js +11 -13
- package/dist/server/services/git-stats-service.js +8 -6
- package/dist/server/services/image-service.js +34 -5
- package/dist/server/services/notion-service.js +45 -5
- package/dist/server/services/pr-watcher-service.js +20 -9
- package/dist/server/services/search-service.js +53 -40
- package/dist/server/services/websocket-service.js +8 -3
- package/dist/server/services/workspace-service.js +81 -10
- package/dist/server/services/worktree-purge-service.js +1 -1
- package/dist/server/utils/git-ops.js +89 -22
- package/dist/server/utils/mcp-client.js +4 -0
- package/dist/server/utils/safe-path.js +14 -0
- package/dist/server/utils/script-runner.js +16 -1
- package/package.json +22 -22
- package/src/client/dist/pwa/assets/{ActivityFeed-D9EfbRqv.css → ActivityFeed-BrjeLlhi.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-UEAexGjc.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-XSYwu9XA.js +1 -0
- package/src/client/dist/pwa/assets/{ClosePopup-_rbZbpfb.js → ClosePopup-dpcrW9DC.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-BK__rjvR.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-QwtfYunr.css +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-CRrDdYBa.css → DiffViewer-CS3P9Ngi.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DnFN6Sc_.js +8 -0
- package/src/client/dist/pwa/assets/HealthPage-DVrZfMI6.js +1 -0
- package/src/client/dist/pwa/assets/MainLayout-D_3nFaCG.js +19 -0
- package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +1 -0
- package/src/client/dist/pwa/assets/QBadge-sMJZ_eZm.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-DedhGMcj.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-BgfN19l8.js +1 -0
- package/src/client/dist/pwa/assets/QChip-EQGYShN0.js +1 -0
- package/src/client/dist/pwa/assets/QIcon--SvgE-nS.js +1 -0
- package/src/client/dist/pwa/assets/QList-Be_FbTA3.js +1 -0
- package/src/client/dist/pwa/assets/QPage-DuIsRmYu.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-CtxxliRU.js +36 -0
- package/src/client/dist/pwa/assets/QSeparator-BdyRE-AM.js +1 -0
- package/src/client/dist/pwa/assets/QSpace-D9UEvM99.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-B4RmFJUj.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-HEIFh5w9.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-1B5HOf90.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-BXiWr2ba.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-v8f4rrCt.js +16 -0
- package/src/client/dist/pwa/assets/TouchPan-lynWXY69.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-ChnyJ3xI.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Vjzi8YnW.js +4 -0
- package/src/client/dist/pwa/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/src/client/dist/pwa/assets/{css.worker-CBlhdqTa.js → css.worker-JKZQyld4.js} +38 -34
- package/src/client/dist/pwa/assets/cssMode-cg8iVI-i.js +1 -0
- package/src/client/dist/pwa/assets/documents-Dlx8yiSO.js +1 -0
- package/src/client/dist/pwa/assets/editor-vZnmUXVO.css +1 -0
- package/src/client/dist/pwa/assets/editor.api-CfaJRkrX.js +847 -0
- package/src/client/dist/pwa/assets/editor.worker-D-m8IBMa.js +30 -0
- package/src/client/dist/pwa/assets/expand-template-D3LZldj1.js +1 -0
- package/src/client/dist/pwa/assets/formatters-D0dU6VcA.js +1 -0
- package/src/client/dist/pwa/assets/{freemarker2-DczqHd5F.js → freemarker2-B_8OIClK.js} +1 -1
- package/src/client/dist/pwa/assets/handlebars-CaYi_uxU.js +1 -0
- package/src/client/dist/pwa/assets/html-BnCUDUl1.js +1 -0
- package/src/client/dist/pwa/assets/html.worker-BUr90sI4.js +506 -0
- package/src/client/dist/pwa/assets/htmlMode-BZjKGnQF.js +1 -0
- package/src/client/dist/pwa/assets/i18n-B6SDs7Lm.js +1 -0
- package/src/client/dist/pwa/assets/index-CK1nk7qr.css +1 -0
- package/src/client/dist/pwa/assets/index-CoOsPyyG.js +82 -0
- package/src/client/dist/pwa/assets/{javascript-Bs83z_W4.js → javascript-DkSfoeHU.js} +1 -1
- package/src/client/dist/pwa/assets/json.worker-BOODjG9U.js +62 -0
- package/src/client/dist/pwa/assets/{jsonMode-Bc-8bzJi.js → jsonMode-yKXxfx1V.js} +3 -3
- package/src/client/dist/pwa/assets/{kobo-commands-B2kM0INN.js → kobo-commands-vn1IQxLn.js} +1 -1
- package/src/client/dist/pwa/assets/layout-CiQpugQB.js +1 -0
- package/src/client/dist/pwa/assets/liquid-Dlhysv3U.js +1 -0
- package/src/client/dist/pwa/assets/lspLanguageFeatures-CeYSQFUz.js +4 -0
- package/src/client/dist/pwa/assets/mdx-B8iME5NA.js +1 -0
- package/src/client/dist/pwa/assets/network-auth-j2Drut6q.js +1 -0
- package/src/client/dist/pwa/assets/pending-diff-open-B4RYz7G_.js +1 -0
- package/src/client/dist/pwa/assets/permissionModes-6jhegAm-.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-DIFZ95_g.js → project-color-e3OdFi9X.js} +1 -1
- package/src/client/dist/pwa/assets/python-CbFbC0ZR.js +1 -0
- package/src/client/dist/pwa/assets/razor-7c2NCdFI.js +1 -0
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DtSZKRQB.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-Fer8kY38.js +67 -0
- package/src/client/dist/pwa/assets/runtime-dom.esm-bundler-DgevF6KV.js +4 -0
- package/src/client/dist/pwa/assets/{ts.worker-PmaSgaZk.js → ts.worker-C-oGa8JT.js} +199 -195
- package/src/client/dist/pwa/assets/{tsMode-DOsxzgZi.js → tsMode-BmShKjbx.js} +1 -1
- package/src/client/dist/pwa/assets/typescript-Dhu7Upbs.js +1 -0
- package/src/client/dist/pwa/assets/use-checkbox-SrkgiJho.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-CTZuBZOi.css +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-fe4sPCNf.js +2 -0
- package/src/client/dist/pwa/assets/use-quasar-CeHlTrGg.js +1 -0
- package/src/client/dist/pwa/assets/vs-B-xsceNQ.js +56 -0
- package/src/client/dist/pwa/assets/vs-JJSFJJVJ.css +1 -0
- package/src/client/dist/pwa/assets/vue-i18n-C3W5i7h6.js +3 -0
- package/src/client/dist/pwa/assets/workers-zVMTXVoC.js +1 -0
- package/src/client/dist/pwa/assets/xml-D64wpCtR.js +1 -0
- package/src/client/dist/pwa/assets/yaml-CLSRKwfx.js +1 -0
- package/src/client/dist/pwa/index.html +8 -8
- package/src/client/dist/pwa/manifest.json +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BkkyAw81.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-BlvhBx6o.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-RSyoQwD7.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-BSNxNex2.js +0 -8
- package/src/client/dist/pwa/assets/HealthPage-CUD1kipV.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-BCLgsn0C.js +0 -37
- package/src/client/dist/pwa/assets/MainLayout-DlS2wxkK.css +0 -1
- package/src/client/dist/pwa/assets/QBadge-DV5kn4mL.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-CmfaeizG.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-C442-yBb.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-xg3LYW7D.js +0 -1
- package/src/client/dist/pwa/assets/QChip-DhUfnmbz.js +0 -1
- package/src/client/dist/pwa/assets/QIcon-C6E-oEjI.js +0 -1
- package/src/client/dist/pwa/assets/QInput-BF8x4RTv.js +0 -1
- package/src/client/dist/pwa/assets/QList-D5EKdFPn.js +0 -1
- package/src/client/dist/pwa/assets/QPage-D3rjwA1l.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-BhyT0k2N.js +0 -36
- package/src/client/dist/pwa/assets/QSeparator-DWTCQxsa.js +0 -1
- package/src/client/dist/pwa/assets/QSpace-ezNrZkuj.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-DIV68-cV.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-ybbFi3KK.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-Bb7TG3Vo.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-C2qe4xKu.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CjPnnn60.js +0 -16
- package/src/client/dist/pwa/assets/ThinkingItem-B1RKyCIS.js +0 -1
- package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +0 -1
- package/src/client/dist/pwa/assets/TouchPan-DzZPYIol.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6Rgjarvf.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bgxg_Rua.css +0 -1
- package/src/client/dist/pwa/assets/build-path-tree-KRACVkD3.js +0 -1
- package/src/client/dist/pwa/assets/chunk-QTnfLwEv.js +0 -1
- package/src/client/dist/pwa/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/src/client/dist/pwa/assets/cssMode-CQsB6mFO.js +0 -1
- package/src/client/dist/pwa/assets/documents-C7efBF5t.js +0 -1
- package/src/client/dist/pwa/assets/editor-BFebQ78e.css +0 -1
- package/src/client/dist/pwa/assets/editor.api2-C58pkxmw.js +0 -872
- package/src/client/dist/pwa/assets/editor.main-BhLqjdq4.js +0 -6
- package/src/client/dist/pwa/assets/editor.worker-Cu3tR8iJ.js +0 -26
- package/src/client/dist/pwa/assets/expand-template-BA-hhG5v.js +0 -1
- package/src/client/dist/pwa/assets/formatters-DugWNOEs.js +0 -1
- package/src/client/dist/pwa/assets/handlebars-DmA7twi8.js +0 -1
- package/src/client/dist/pwa/assets/html-DtZDrViU.js +0 -1
- package/src/client/dist/pwa/assets/html.worker-BKBrNna1.js +0 -502
- package/src/client/dist/pwa/assets/htmlMode-DO9_qUdT.js +0 -1
- package/src/client/dist/pwa/assets/i18n-CipNqHTm.js +0 -1
- package/src/client/dist/pwa/assets/index-BxspWSDs.js +0 -84
- package/src/client/dist/pwa/assets/index-CwYR3iHt.css +0 -1
- package/src/client/dist/pwa/assets/json.worker-BaLYt9Ss.js +0 -58
- package/src/client/dist/pwa/assets/layout-C6frIxKZ.js +0 -1
- package/src/client/dist/pwa/assets/liquid-Dnxe0NUB.js +0 -1
- package/src/client/dist/pwa/assets/lspLanguageFeatures-qDBRYovw.js +0 -4
- package/src/client/dist/pwa/assets/mdx-C1oke9bq.js +0 -1
- package/src/client/dist/pwa/assets/monaco.contribution-BbFzrpO3.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-Bj1LS5R7.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-C3voSObg.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-DMuXGtZu.js +0 -1
- package/src/client/dist/pwa/assets/python-Dgq-bJSM.js +0 -1
- package/src/client/dist/pwa/assets/razor-Bms4CQGF.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-BRSTe4je.js +0 -66
- package/src/client/dist/pwa/assets/typescript-CzOp_GqX.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-E9SV7iH5.js +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-B3l7mx48.css +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-BGy3sOAN.js +0 -2
- package/src/client/dist/pwa/assets/use-quasar-DsmzxifG.js +0 -1
- package/src/client/dist/pwa/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -3
- package/src/client/dist/pwa/assets/workers-0fozwhvS.js +0 -1
- package/src/client/dist/pwa/assets/xml-CPa01uHm.js +0 -1
- package/src/client/dist/pwa/assets/yaml-Cw3fzSuh.js +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
3
4
|
import { Hono } from 'hono';
|
|
4
5
|
import { getDb } from '../db/index.js';
|
|
5
6
|
import { SCHEMA_VERSION } from '../db/migrations.js';
|
|
@@ -9,24 +10,22 @@ import { getDbPath, getKoboHome, getPackageVersion } from '../utils/paths.js';
|
|
|
9
10
|
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
10
11
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
11
12
|
const app = new Hono();
|
|
12
|
-
|
|
13
|
+
const execFileAsync = promisify(execFileCb);
|
|
14
|
+
async function checkCli(binary) {
|
|
13
15
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return { available: false, version: null };
|
|
17
|
-
return { available: true, version: (r.stdout ?? '').trim() || null };
|
|
16
|
+
const { stdout } = await execFileAsync(binary, ['--version'], { encoding: 'utf-8', timeout: 2_000 });
|
|
17
|
+
return { available: true, version: stdout.trim() || null };
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
return { available: false, version: null };
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
function
|
|
23
|
+
function checkClaudeCli() {
|
|
24
|
+
return checkCli('claude');
|
|
25
|
+
}
|
|
26
|
+
async function checkCodexCli() {
|
|
24
27
|
try {
|
|
25
|
-
|
|
26
|
-
const r = spawnSync(bin, ['--version'], { encoding: 'utf-8' });
|
|
27
|
-
if (r.error || r.status !== 0)
|
|
28
|
-
return { available: false, version: null };
|
|
29
|
-
return { available: true, version: (r.stdout ?? '').trim() || null };
|
|
28
|
+
return await checkCli(resolveCodexBinary());
|
|
30
29
|
}
|
|
31
30
|
catch {
|
|
32
31
|
return { available: false, version: null };
|
|
@@ -50,10 +49,11 @@ function safeFileSize(p) {
|
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
// GET /api/health/report — detailed health diagnostics for the Health panel.
|
|
53
|
-
app.get('/report', (c) => {
|
|
52
|
+
app.get('/report', async (c) => {
|
|
54
53
|
const db = getDb();
|
|
55
54
|
const dbPath = getDbPath();
|
|
56
55
|
const home = getKoboHome();
|
|
56
|
+
const cliChecks = Promise.all([checkClaudeCli(), checkCodexCli()]);
|
|
57
57
|
// DB schema version
|
|
58
58
|
const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get();
|
|
59
59
|
const dbSchemaVersion = row?.v ?? 0;
|
|
@@ -145,6 +145,7 @@ app.get('/report', (c) => {
|
|
|
145
145
|
.all();
|
|
146
146
|
const settingsRow = db.prepare('SELECT COUNT(*) as n FROM workspaces').get();
|
|
147
147
|
const archivedRow = db.prepare('SELECT COUNT(*) as n FROM workspaces WHERE archived_at IS NOT NULL').get();
|
|
148
|
+
const [claudeCli, codexCli] = await cliChecks;
|
|
148
149
|
const report = {
|
|
149
150
|
version: getPackageVersion(),
|
|
150
151
|
koboHome: home,
|
|
@@ -155,8 +156,8 @@ app.get('/report', (c) => {
|
|
|
155
156
|
currentSchemaVersion: SCHEMA_VERSION,
|
|
156
157
|
},
|
|
157
158
|
settings: { schemaVersion: SETTINGS_SCHEMA_VERSION },
|
|
158
|
-
claudeCli
|
|
159
|
-
codexCli
|
|
159
|
+
claudeCli,
|
|
160
|
+
codexCli,
|
|
160
161
|
workspaces: {
|
|
161
162
|
total: settingsRow.n,
|
|
162
163
|
archived: archivedRow.n,
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
import * as imageService from '../services/image-service.js';
|
|
5
5
|
import * as workspaceService from '../services/workspace-service.js';
|
|
6
|
+
import { isPathInside } from '../utils/safe-path.js';
|
|
6
7
|
/** Maximum allowed upload size for a single image (10 MB). */
|
|
7
8
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
8
9
|
/** MIME types accepted for image uploads. */
|
|
@@ -67,7 +68,7 @@ app.get('/:id/images/file', async (c) => {
|
|
|
67
68
|
}
|
|
68
69
|
// Allowlist: only the upload storage layout is served. No symlink escape
|
|
69
70
|
// either — we resolve and check containment.
|
|
70
|
-
if (
|
|
71
|
+
if (!/^\.ai\/images\/[^/]+$/.test(requested) || requested.includes('..')) {
|
|
71
72
|
return c.json({ error: 'Invalid or disallowed image path' }, 400);
|
|
72
73
|
}
|
|
73
74
|
const worktreePath = workspace.worktreePath;
|
|
@@ -78,9 +79,23 @@ app.get('/:id/images/file', async (c) => {
|
|
|
78
79
|
if (fullPath !== imagesRoot && !fullPath.startsWith(imagesRoot + path.sep)) {
|
|
79
80
|
return c.json({ error: 'Path escapes images root' }, 400);
|
|
80
81
|
}
|
|
82
|
+
if (!fs.existsSync(imagesRoot) || fs.lstatSync(imagesRoot).isSymbolicLink()) {
|
|
83
|
+
return c.json({ error: 'Invalid images root' }, 400);
|
|
84
|
+
}
|
|
85
|
+
if (!fs.existsSync(fullPath))
|
|
86
|
+
return c.json({ error: 'Image not found' }, 404);
|
|
87
|
+
const fileStat = fs.lstatSync(fullPath);
|
|
88
|
+
if (fileStat.isSymbolicLink())
|
|
89
|
+
return c.json({ error: 'Symbolic links are not allowed' }, 400);
|
|
90
|
+
const realWorktree = await fs.promises.realpath(worktreePath);
|
|
91
|
+
const realImagesRoot = await fs.promises.realpath(imagesRoot);
|
|
92
|
+
const realFile = await fs.promises.realpath(fullPath);
|
|
93
|
+
if (!isPathInside(realWorktree, realImagesRoot) || !isPathInside(realImagesRoot, realFile)) {
|
|
94
|
+
return c.json({ error: 'Path escapes images root' }, 400);
|
|
95
|
+
}
|
|
81
96
|
let buffer;
|
|
82
97
|
try {
|
|
83
|
-
buffer = await fs.promises.readFile(
|
|
98
|
+
buffer = await fs.promises.readFile(realFile);
|
|
84
99
|
}
|
|
85
100
|
catch {
|
|
86
101
|
return c.json({ error: 'Image not found' }, 404);
|
|
@@ -48,9 +48,23 @@ const app = new Hono();
|
|
|
48
48
|
/** Tracks workspaces currently running a setup script to prevent concurrent executions. */
|
|
49
49
|
const setupScriptRunning = new Set();
|
|
50
50
|
const VALID_AGENT_PERMISSION_MODES = ['plan', 'bypass', 'strict', 'interactive'];
|
|
51
|
+
const VALID_WORKSPACE_STATUSES = [
|
|
52
|
+
'created',
|
|
53
|
+
'extracting',
|
|
54
|
+
'brainstorming',
|
|
55
|
+
'executing',
|
|
56
|
+
'awaiting-user',
|
|
57
|
+
'completed',
|
|
58
|
+
'idle',
|
|
59
|
+
'error',
|
|
60
|
+
'quota',
|
|
61
|
+
];
|
|
51
62
|
function isAgentPermissionMode(value) {
|
|
52
63
|
return typeof value === 'string' && VALID_AGENT_PERMISSION_MODES.includes(value);
|
|
53
64
|
}
|
|
65
|
+
function isWorkspaceStatus(value) {
|
|
66
|
+
return typeof value === 'string' && VALID_WORKSPACE_STATUSES.includes(value);
|
|
67
|
+
}
|
|
54
68
|
async function deliverAgentPrompt(workspace, workingDir, prompt) {
|
|
55
69
|
const delivery = await agentManager.sendMessageForFallback(workspace.id, prompt);
|
|
56
70
|
if (delivery.status === 'sent')
|
|
@@ -1650,29 +1664,19 @@ app.get('/:id/session-metrics', (c) => {
|
|
|
1650
1664
|
session.id,
|
|
1651
1665
|
{ sessionId: session.id, toolCalls: 0, errors: 0, inputTokens: 0, outputTokens: 0 },
|
|
1652
1666
|
]));
|
|
1653
|
-
const
|
|
1654
|
-
.prepare(
|
|
1667
|
+
const rows = db
|
|
1668
|
+
.prepare(`SELECT session_id, tool_calls, errors, input_tokens, output_tokens
|
|
1669
|
+
FROM session_event_metrics
|
|
1670
|
+
WHERE workspace_id = ?`)
|
|
1655
1671
|
.all(id);
|
|
1656
|
-
for (const row of
|
|
1672
|
+
for (const row of rows) {
|
|
1657
1673
|
const metric = metrics.get(row.session_id);
|
|
1658
1674
|
if (!metric)
|
|
1659
1675
|
continue;
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
if (event.kind === 'error' || (event.kind === 'tool:result' && event.isError === true))
|
|
1665
|
-
metric.errors++;
|
|
1666
|
-
if (event.kind === 'usage') {
|
|
1667
|
-
if (typeof event.inputTokens === 'number')
|
|
1668
|
-
metric.inputTokens = Math.max(metric.inputTokens, event.inputTokens);
|
|
1669
|
-
if (typeof event.outputTokens === 'number')
|
|
1670
|
-
metric.outputTokens = Math.max(metric.outputTokens, event.outputTokens);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
catch {
|
|
1674
|
-
// Corrupt historical events are intentionally ignored in diagnostics.
|
|
1675
|
-
}
|
|
1676
|
+
metric.toolCalls = row.tool_calls;
|
|
1677
|
+
metric.errors = row.errors;
|
|
1678
|
+
metric.inputTokens = row.input_tokens;
|
|
1679
|
+
metric.outputTokens = row.output_tokens;
|
|
1676
1680
|
}
|
|
1677
1681
|
return c.json({ sessions, metrics: [...metrics.values()] });
|
|
1678
1682
|
}
|
|
@@ -1719,7 +1723,8 @@ app.get('/:id/events', (c) => {
|
|
|
1719
1723
|
// optional: scope to a session view. Session views also include
|
|
1720
1724
|
// workspace-level rows where session_id IS NULL (legacy/pre-session items).
|
|
1721
1725
|
const session = c.req.query('session');
|
|
1722
|
-
const
|
|
1726
|
+
const parsedLimit = parseInt(c.req.query('limit') ?? '100', 10);
|
|
1727
|
+
const limit = Math.max(1, Math.min(Number.isFinite(parsedLimit) ? parsedLimit : 100, 500));
|
|
1723
1728
|
const db = getDb();
|
|
1724
1729
|
let rows;
|
|
1725
1730
|
if (around) {
|
|
@@ -1758,12 +1763,12 @@ app.get('/:id/events', (c) => {
|
|
|
1758
1763
|
.prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) ORDER BY rowid DESC LIMIT ?')
|
|
1759
1764
|
.all(id, session, limit)
|
|
1760
1765
|
: db
|
|
1761
|
-
.prepare('SELECT * FROM ws_events WHERE workspace_id = ? ORDER BY rowid
|
|
1766
|
+
.prepare('SELECT * FROM ws_events WHERE workspace_id = ? ORDER BY rowid DESC LIMIT ?')
|
|
1762
1767
|
.all(id, limit);
|
|
1763
1768
|
}
|
|
1764
1769
|
// Reverse to chronological order (we queried DESC for "before" pagination,
|
|
1765
1770
|
// or for the "session + no cursor" case where we fetched the newest first).
|
|
1766
|
-
if (
|
|
1771
|
+
if (!around)
|
|
1767
1772
|
rows.reverse();
|
|
1768
1773
|
const events = rows.map((row) => {
|
|
1769
1774
|
let parsedPayload;
|
|
@@ -1785,7 +1790,7 @@ app.get('/:id/events', (c) => {
|
|
|
1785
1790
|
// Check if there are more older events beyond what we returned
|
|
1786
1791
|
let hasMore = false;
|
|
1787
1792
|
if (rows.length > 0) {
|
|
1788
|
-
if (before) {
|
|
1793
|
+
if (before || around) {
|
|
1789
1794
|
const firstRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ?').get(rows[0].id);
|
|
1790
1795
|
if (firstRow) {
|
|
1791
1796
|
const older = session
|
|
@@ -1798,10 +1803,12 @@ app.get('/:id/events', (c) => {
|
|
|
1798
1803
|
hasMore = older.c > 0;
|
|
1799
1804
|
}
|
|
1800
1805
|
}
|
|
1801
|
-
else
|
|
1802
|
-
const total =
|
|
1803
|
-
|
|
1804
|
-
|
|
1806
|
+
else {
|
|
1807
|
+
const total = session
|
|
1808
|
+
? db
|
|
1809
|
+
.prepare('SELECT COUNT(*) as c FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL)')
|
|
1810
|
+
.get(id, session)
|
|
1811
|
+
: db.prepare('SELECT COUNT(*) as c FROM ws_events WHERE workspace_id = ?').get(id);
|
|
1805
1812
|
hasMore = total.c > rows.length;
|
|
1806
1813
|
}
|
|
1807
1814
|
}
|
|
@@ -2017,7 +2024,11 @@ app.put('/:id/tags', async (c) => {
|
|
|
2017
2024
|
app.patch('/:id', migrationGuard, async (c) => {
|
|
2018
2025
|
try {
|
|
2019
2026
|
const id = c.req.param('id');
|
|
2020
|
-
const
|
|
2027
|
+
const parsed = await c.req.json();
|
|
2028
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
2029
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
2030
|
+
}
|
|
2031
|
+
const body = parsed;
|
|
2021
2032
|
const workspace = workspaceService.getWorkspace(id);
|
|
2022
2033
|
if (!workspace) {
|
|
2023
2034
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
@@ -2026,12 +2037,12 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
2026
2037
|
if ('agent_description' in body) {
|
|
2027
2038
|
return c.json({ error: 'agent_description must be set via the agent MCP tool, not via the API' }, 400);
|
|
2028
2039
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
updated = workspaceService.updateWorkspaceModel(id, body.model);
|
|
2040
|
+
if (body.model !== undefined && (typeof body.model !== 'string' || !body.model.trim())) {
|
|
2041
|
+
return c.json({ error: 'model must be a non-empty string' }, 400);
|
|
2032
2042
|
}
|
|
2033
|
-
if (body.reasoningEffort !== undefined
|
|
2034
|
-
|
|
2043
|
+
if (body.reasoningEffort !== undefined &&
|
|
2044
|
+
(typeof body.reasoningEffort !== 'string' || !body.reasoningEffort.trim())) {
|
|
2045
|
+
return c.json({ error: 'reasoningEffort must be a non-empty string' }, 400);
|
|
2035
2046
|
}
|
|
2036
2047
|
if (body.agentPermissionMode !== undefined) {
|
|
2037
2048
|
if (!isAgentPermissionMode(body.agentPermissionMode)) {
|
|
@@ -2048,25 +2059,25 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
2048
2059
|
error: `Engine '${engineId}' does not support agentPermissionMode '${body.agentPermissionMode}'. Supported: ${supported.join(', ')}`,
|
|
2049
2060
|
}, 400);
|
|
2050
2061
|
}
|
|
2051
|
-
updated = workspaceService.updateAgentPermissionMode(id, body.agentPermissionMode);
|
|
2052
2062
|
}
|
|
2053
|
-
if (body.status) {
|
|
2063
|
+
if (body.status !== undefined) {
|
|
2064
|
+
if (!isWorkspaceStatus(body.status)) {
|
|
2065
|
+
return c.json({ error: `Invalid status. Must be one of: ${VALID_WORKSPACE_STATUSES.join(', ')}` }, 400);
|
|
2066
|
+
}
|
|
2054
2067
|
if (body.status === 'idle' && agentManager.hasController(id)) {
|
|
2055
2068
|
return c.json({ error: 'Cannot mark an active agent workspace idle; end or stop the agent session first' }, 409);
|
|
2056
2069
|
}
|
|
2057
|
-
updated = workspaceService.updateWorkspaceStatus(id, body.status);
|
|
2058
2070
|
}
|
|
2059
|
-
if (body.name !== undefined) {
|
|
2060
|
-
|
|
2071
|
+
if (body.name !== undefined && typeof body.name !== 'string') {
|
|
2072
|
+
return c.json({ error: 'name must be a string' }, 400);
|
|
2061
2073
|
}
|
|
2062
2074
|
if ('description' in body) {
|
|
2063
2075
|
const desc = body.description;
|
|
2064
2076
|
if (desc !== null && typeof desc !== 'string') {
|
|
2065
2077
|
return c.json({ error: 'description must be a string or null' }, 400);
|
|
2066
2078
|
}
|
|
2067
|
-
updated = workspaceService.updateWorkspaceDescription(id, desc);
|
|
2068
2079
|
}
|
|
2069
|
-
if (
|
|
2080
|
+
if (body.status === undefined &&
|
|
2070
2081
|
body.model === undefined &&
|
|
2071
2082
|
body.reasoningEffort === undefined &&
|
|
2072
2083
|
body.agentPermissionMode === undefined &&
|
|
@@ -2074,7 +2085,7 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
2074
2085
|
!('description' in body)) {
|
|
2075
2086
|
return c.json({ error: 'Missing field: status, model, reasoningEffort, agentPermissionMode, name, or description' }, 400);
|
|
2076
2087
|
}
|
|
2077
|
-
return c.json(
|
|
2088
|
+
return c.json(workspaceService.updateWorkspaceFields(id, body));
|
|
2078
2089
|
}
|
|
2079
2090
|
catch (err) {
|
|
2080
2091
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2270,7 +2281,7 @@ app.post('/:id/run-setup-script', async (c) => {
|
|
|
2270
2281
|
}
|
|
2271
2282
|
});
|
|
2272
2283
|
// POST /api/workspaces/:id/archive — mark workspace as archived (soft-delete)
|
|
2273
|
-
app.post('/:id/archive', migrationGuard, (c) => {
|
|
2284
|
+
app.post('/:id/archive', migrationGuard, async (c) => {
|
|
2274
2285
|
try {
|
|
2275
2286
|
const id = c.req.param('id');
|
|
2276
2287
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -2287,7 +2298,7 @@ app.post('/:id/archive', migrationGuard, (c) => {
|
|
|
2287
2298
|
// Agent may not be running — ignore
|
|
2288
2299
|
}
|
|
2289
2300
|
try {
|
|
2290
|
-
devServerService.stopDevServer(id);
|
|
2301
|
+
await devServerService.stopDevServer(id);
|
|
2291
2302
|
}
|
|
2292
2303
|
catch (err) {
|
|
2293
2304
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2362,7 +2373,7 @@ app.post('/:id/unarchive', migrationGuard, (c) => {
|
|
|
2362
2373
|
// Every side-effect is best-effort — failures are collected as warnings
|
|
2363
2374
|
// rather than thrown, so a bulk delete never aborts mid-batch. Returns the
|
|
2364
2375
|
// list of user-facing warning messages (empty when everything was clean).
|
|
2365
|
-
function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
2376
|
+
async function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
2366
2377
|
// Stop agent if running (best-effort)
|
|
2367
2378
|
try {
|
|
2368
2379
|
agentManager.stopAgent(workspace.id);
|
|
@@ -2373,7 +2384,7 @@ function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
2373
2384
|
// Stop dev server if it was running. The processSpawn would otherwise
|
|
2374
2385
|
// outlive the workspace (and keep its port + docker containers alive).
|
|
2375
2386
|
try {
|
|
2376
|
-
devServerService.stopDevServer(workspace.id);
|
|
2387
|
+
await devServerService.stopDevServer(workspace.id);
|
|
2377
2388
|
}
|
|
2378
2389
|
catch (err) {
|
|
2379
2390
|
console.error(`[workspaces] stopDevServer during delete failed for '${workspace.name}':`, err);
|
|
@@ -2457,7 +2468,7 @@ app.delete('/archived', migrationGuard, async (c) => {
|
|
|
2457
2468
|
let deleted = 0;
|
|
2458
2469
|
for (const workspace of archived) {
|
|
2459
2470
|
try {
|
|
2460
|
-
warnings.push(...deleteWorkspaceWithSideEffects(workspace, body));
|
|
2471
|
+
warnings.push(...(await deleteWorkspaceWithSideEffects(workspace, body)));
|
|
2461
2472
|
deleted++;
|
|
2462
2473
|
}
|
|
2463
2474
|
catch (err) {
|
|
@@ -2485,7 +2496,7 @@ app.delete('/:id', migrationGuard, async (c) => {
|
|
|
2485
2496
|
const body = await c.req
|
|
2486
2497
|
.json()
|
|
2487
2498
|
.catch(() => ({}));
|
|
2488
|
-
const warnings = deleteWorkspaceWithSideEffects(workspace, body);
|
|
2499
|
+
const warnings = await deleteWorkspaceWithSideEffects(workspace, body);
|
|
2489
2500
|
// When everything worked cleanly we keep the legacy 204 response so
|
|
2490
2501
|
// existing clients aren't surprised by a JSON body. Warnings promote the
|
|
2491
2502
|
// response to 200 so the body is readable.
|
|
@@ -2507,6 +2518,10 @@ app.post('/:id/start', migrationGuard, async (c) => {
|
|
|
2507
2518
|
if (!workspace) {
|
|
2508
2519
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2509
2520
|
}
|
|
2521
|
+
if (workspace.archivedAt)
|
|
2522
|
+
return c.json({ error: `Workspace '${id}' is archived` }, 409);
|
|
2523
|
+
if (workspace.worktreePurgedAt)
|
|
2524
|
+
return c.json({ error: `Workspace '${id}' worktree is purged` }, 409);
|
|
2510
2525
|
// If the workspace declares an engine, ensure it is still registered.
|
|
2511
2526
|
// Otherwise startAgent() would throw from deep inside resolveEngine and
|
|
2512
2527
|
// surface as an opaque 500 — better to fail fast with a clear 400.
|
|
@@ -3300,7 +3315,7 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
3300
3315
|
}, 409);
|
|
3301
3316
|
}
|
|
3302
3317
|
const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
|
|
3303
|
-
if (
|
|
3318
|
+
if (snapshot?.state !== 'OPEN') {
|
|
3304
3319
|
return c.json({ error: 'No open PR found for this workspace', code: 'pr_not_open' }, 409);
|
|
3305
3320
|
}
|
|
3306
3321
|
if (!snapshot.readyToMerge) {
|
|
@@ -3654,7 +3669,7 @@ app.post('/:id/start-ci-fix', migrationGuard, async (c) => {
|
|
|
3654
3669
|
if (!snapshot) {
|
|
3655
3670
|
snapshot = getAllPrSnapshots()[id] ?? null;
|
|
3656
3671
|
}
|
|
3657
|
-
if (
|
|
3672
|
+
if (snapshot?.state !== 'OPEN' || snapshot.ci.rollup !== 'FAILURE') {
|
|
3658
3673
|
return c.json({ error: 'No failing CI detected on this workspace' }, 400);
|
|
3659
3674
|
}
|
|
3660
3675
|
const effective = settingsService.getEffectiveSettings(workspace.projectPath);
|
|
@@ -378,7 +378,11 @@ export function createCodexEngine() {
|
|
|
378
378
|
if (!pending)
|
|
379
379
|
return false;
|
|
380
380
|
pendingByCallId.delete(callId);
|
|
381
|
-
|
|
381
|
+
// Only resume the idle-timeout clock once every outstanding
|
|
382
|
+
// approval has been answered — a sibling request may still be
|
|
383
|
+
// waiting on a human decision.
|
|
384
|
+
if (pendingByCallId.size === 0)
|
|
385
|
+
turnLiveness.resume();
|
|
382
386
|
const result = buildResponseForResolve(pending, response);
|
|
383
387
|
client.peer.respond(pending.requestId, result);
|
|
384
388
|
return true;
|
|
@@ -847,6 +847,13 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
847
847
|
* query the controller via `_getControllers()` for tests.
|
|
848
848
|
*/
|
|
849
849
|
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort) {
|
|
850
|
+
const workspace = getWs(workspaceId);
|
|
851
|
+
if (!workspace)
|
|
852
|
+
throw new Error(`Workspace '${workspaceId}' not found`);
|
|
853
|
+
if (workspace.archivedAt)
|
|
854
|
+
throw new Error(`Workspace '${workspaceId}' is archived`);
|
|
855
|
+
if (workspace.worktreePurgedAt)
|
|
856
|
+
throw new Error(`Workspace '${workspaceId}' worktree is purged`);
|
|
850
857
|
// "For this turn" never survives a new agent session.
|
|
851
858
|
permissionPolicyService.clearTurnPermissions(workspaceId);
|
|
852
859
|
// Zombie detection: an SDK iterator hung on a never-resolved canUseTool
|
|
@@ -1317,7 +1324,7 @@ export function _getPendingDeferred() {
|
|
|
1317
1324
|
const out = new Map();
|
|
1318
1325
|
for (const [wid, arr] of pendingQueue) {
|
|
1319
1326
|
const head = arr[0];
|
|
1320
|
-
if (
|
|
1327
|
+
if (head?.kind !== 'question')
|
|
1321
1328
|
continue;
|
|
1322
1329
|
out.set(wid, {
|
|
1323
1330
|
toolCallId: head.toolCallId,
|
|
@@ -78,7 +78,7 @@ export function enable(workspaceId) {
|
|
|
78
78
|
*/
|
|
79
79
|
export function disable(workspaceId, reason) {
|
|
80
80
|
const row = getRow(workspaceId);
|
|
81
|
-
if (
|
|
81
|
+
if (row?.auto_loop !== 1)
|
|
82
82
|
return;
|
|
83
83
|
const db = getDb();
|
|
84
84
|
db.prepare('UPDATE workspaces SET auto_loop = 0 WHERE id = ?').run(workspaceId);
|
|
@@ -75,13 +75,34 @@ export async function changeSourceBranch(workspaceId, newBase) {
|
|
|
75
75
|
updateWorkspaceSourceBranch(workspaceId, trimmedNew);
|
|
76
76
|
return { status: 'conflict', forcePushNeeded, commitCount: commits.length };
|
|
77
77
|
}
|
|
78
|
+
// Non-conflict failure (e.g. the `reset --hard` itself failed) — best
|
|
79
|
+
// effort restore of the stash before letting the error propagate, same
|
|
80
|
+
// as the original `finally` did, so the user doesn't lose uncommitted
|
|
81
|
+
// work on an unrelated failure.
|
|
82
|
+
if (stashed) {
|
|
83
|
+
try {
|
|
84
|
+
gitOps.stashPop(worktreePath);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* best-effort — the original error is more relevant than this one */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
78
90
|
throw err;
|
|
79
91
|
}
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
// The reset/reconstruct already landed on disk at this point. Update the
|
|
93
|
+
// DB's source_branch BEFORE attempting the stash pop: if the pop conflicts
|
|
94
|
+
// (the stashed local edits collide with the new base's content), the
|
|
95
|
+
// worktree and DB must not disagree about which base it's on — the user
|
|
96
|
+
// resolves the stash conflict manually, same as a cherry-pick conflict.
|
|
97
|
+
updateWorkspaceSourceBranch(workspaceId, trimmedNew);
|
|
98
|
+
if (stashed) {
|
|
99
|
+
try {
|
|
82
100
|
gitOps.stashPop(worktreePath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { status: 'conflict', forcePushNeeded, commitCount: commits.length };
|
|
104
|
+
}
|
|
83
105
|
}
|
|
84
|
-
updateWorkspaceSourceBranch(workspaceId, trimmedNew);
|
|
85
106
|
try {
|
|
86
107
|
const provider = getForgeProvider(resolveForge(workspace.projectPath));
|
|
87
108
|
if (provider.capabilities.canChangePrBase) {
|
|
@@ -126,8 +147,25 @@ async function runCustomScript(workspace, oldBase, newBase, script) {
|
|
|
126
147
|
KOBO_FORGE: forgeId,
|
|
127
148
|
KOBO_PR_NUMBER: prNumber,
|
|
128
149
|
},
|
|
150
|
+
// Detached so `child` leads its own process group. Node's native
|
|
151
|
+
// `timeout` option below only signals `child` itself — without this,
|
|
152
|
+
// a script that backgrounds a long-running command (`docker compose
|
|
153
|
+
// up -d &`) leaves it running as an orphan after the timeout fires.
|
|
154
|
+
detached: true,
|
|
129
155
|
timeout: SCRIPT_TIMEOUT_MS,
|
|
156
|
+
killSignal: 'SIGTERM',
|
|
130
157
|
});
|
|
158
|
+
// Node's `timeout` option kills only `child`. Mirror that same signal to
|
|
159
|
+
// the whole process group so a backgrounded child dies with it.
|
|
160
|
+
const groupKillTimer = setTimeout(() => {
|
|
161
|
+
try {
|
|
162
|
+
if (child.pid)
|
|
163
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* process group already gone */
|
|
167
|
+
}
|
|
168
|
+
}, SCRIPT_TIMEOUT_MS);
|
|
131
169
|
let stderrBuf = '';
|
|
132
170
|
child.stderr?.on('data', (chunk) => {
|
|
133
171
|
stderrBuf += chunk.toString();
|
|
@@ -135,9 +173,11 @@ async function runCustomScript(workspace, oldBase, newBase, script) {
|
|
|
135
173
|
stderrBuf = stderrBuf.slice(-8 * 1024);
|
|
136
174
|
});
|
|
137
175
|
child.on('error', (err) => {
|
|
176
|
+
clearTimeout(groupKillTimer);
|
|
138
177
|
reject(new Error(`Custom change-source-branch script failed to spawn: ${err.message}`));
|
|
139
178
|
});
|
|
140
179
|
child.on('exit', (code, signal) => {
|
|
180
|
+
clearTimeout(groupKillTimer);
|
|
141
181
|
if (code === 0) {
|
|
142
182
|
updateWorkspaceSourceBranch(workspace.id, newBase);
|
|
143
183
|
resolve({ status: 'done', forcePushNeeded: false, commitCount: 0 });
|