@loicngr/kobo 1.7.14 → 1.7.15
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/CHANGELOG.md +25 -0
- package/README.md +5 -3
- package/dist/server/db/migrations.js +33 -0
- package/dist/server/index.js +21 -2
- package/dist/server/routes/changelog.js +47 -0
- package/dist/server/routes/export.js +71 -0
- package/dist/server/routes/fs.js +49 -0
- package/dist/server/routes/git.js +16 -1
- package/dist/server/routes/workspaces.js +107 -64
- package/dist/server/services/agent/orchestrator.js +10 -0
- package/dist/server/services/archive-script-service.js +65 -0
- package/dist/server/services/auto-loop-service.js +7 -0
- package/dist/server/services/cleanup-script-service.js +118 -0
- package/dist/server/services/settings-service.js +184 -2
- package/dist/server/services/setup-script-service.js +10 -81
- package/dist/server/utils/git-ops.js +30 -0
- package/dist/server/utils/paths.js +4 -0
- package/dist/server/utils/script-runner.js +96 -0
- package/package.json +2 -1
- package/src/client/dist/spa/assets/ActivityFeed-DU6lDEP0.js +8 -0
- package/src/client/dist/spa/assets/{ActivityFeed-WjiQ9716.css → ActivityFeed-yUMQhnW4.css} +1 -1
- package/src/client/dist/spa/assets/{ClosePopup-C5EF3QrA.js → ClosePopup-CxvZA3ft.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-CdZr7f3j.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-DgHjL4cZ.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-B-pWVglv.js → DiffViewer-m801GPfI.js} +3 -3
- package/src/client/dist/spa/assets/HealthPage-z1uIOpYk.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-Bp0oVWa-.css → MainLayout-BJmBXwYn.css} +1 -1
- package/src/client/dist/spa/assets/MainLayout-oJdQ-QKM.js +37 -0
- package/src/client/dist/spa/assets/{QBadge-u0mEz_W1.js → QBadge-NEwszYs7.js} +1 -1
- package/src/client/dist/spa/assets/QBanner-Jsq4uJZs.js +1 -0
- package/src/client/dist/spa/assets/QBtn-CoU-UC_j.js +1 -0
- package/src/client/dist/spa/assets/{QCheckbox-skYuqkHX.js → QCheckbox-Cq2STfHp.js} +1 -1
- package/src/client/dist/spa/assets/{QChip-DSMeriN6.js → QChip-DnJyQVs2.js} +2 -2
- package/src/client/dist/spa/assets/QExpansionItem-BTd5m2yV.js +1 -0
- package/src/client/dist/spa/assets/QIcon-BmEX2rXO.js +1 -0
- package/src/client/dist/spa/assets/QInput-D0t39uK_.js +1 -0
- package/src/client/dist/spa/assets/{QItemLabel-B0tYxHQg.js → QItemLabel-Btqw0P7M.js} +1 -1
- package/src/client/dist/spa/assets/QItemSection-DRg-QuAD.js +1 -0
- package/src/client/dist/spa/assets/QList-B3TuWSqL.js +1 -0
- package/src/client/dist/spa/assets/QMenu-C2Wwwf2E.js +1 -0
- package/src/client/dist/spa/assets/QPage-DO_bQyV_.js +1 -0
- package/src/client/dist/spa/assets/QRadio-M9mC5jZy.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-A1wI0IXU.js +1 -0
- package/src/client/dist/spa/assets/QSpace-DONPiIes.js +1 -0
- package/src/client/dist/spa/assets/{QSpinnerDots-CluOpUgq.js → QSpinnerDots-DspFKwCZ.js} +1 -1
- package/src/client/dist/spa/assets/{QToggle-aBvIHg6j.js → QToggle-DBzTAIbK.js} +1 -1
- package/src/client/dist/spa/assets/QTooltip-Bfdmzm_m.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-ChmKHNKn.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-B59LoCos.js +9 -0
- package/src/client/dist/spa/assets/{SettingsPage-B_qPRSDH.css → SettingsPage-BJLyYrBN.css} +1 -1
- package/src/client/dist/spa/assets/{TouchPan-DPBZDRzA.js → TouchPan-BIE5rs7U.js} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-Bj1PJSWT.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-tFBswKV9.css +1 -0
- package/src/client/dist/spa/assets/build-path-tree-BGUV3nY1.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-D5ZOAlu6.js → cssMode-BU4X8R6a.js} +1 -1
- package/src/client/dist/spa/assets/documents-B3nitIYF.js +1 -0
- package/src/client/dist/spa/assets/{editor.api-CTE0T74t.js → editor.api-B4xBDzmJ.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-Buyn4kmD.js → editor.main-CSZRkloL.js} +3 -3
- package/src/client/dist/spa/assets/engineFeatures-CLOVr5b4.js +1 -0
- package/src/client/dist/spa/assets/expand-template-BxUkuL5g.js +1 -0
- package/src/client/dist/spa/assets/{formatters-CgfY9uSI.js → formatters-9dcj2tyJ.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-BqQ6Og68.js → freemarker2-DRz20wAV.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-DuIKUFde.js → handlebars-C0dsvPnC.js} +1 -1
- package/src/client/dist/spa/assets/{html-BHWxRlmb.js → html-Cqvj1pWs.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-C-SjHXMn.js → htmlMode-BTHNvkm6.js} +1 -1
- package/src/client/dist/spa/assets/i18n-D1I-Us2H.js +1 -0
- package/src/client/dist/spa/assets/{index-5ydpLSpt.css → index-ClabzOPc.css} +1 -1
- package/src/client/dist/spa/assets/index-KABmOIkF.js +2 -0
- package/src/client/dist/spa/assets/{javascript-ukUWOwiC.js → javascript-C8n3U02v.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-C_hd-E3X.js → jsonMode-C3AFxQ6K.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-Bh5k0Smw.js → kobo-commands-BuxgteGZ.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-B6flz_LR.js → liquid-C4wtUDrJ.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-F-Hst5AO.js → mdx-CaT1p1F2.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-C8xDzD2M.js → monaco.contribution-CJg5GKVf.js} +2 -2
- package/src/client/dist/spa/assets/notifications-BC6en6Lt.js +1 -0
- package/src/client/dist/spa/assets/permissionModes-BQHBTBwa.js +1 -0
- package/src/client/dist/spa/assets/{python-Bl42f-yd.js → python-Cj54W2Tg.js} +1 -1
- package/src/client/dist/spa/assets/{razor-Dx7_W971.js → razor-D3gJxoX_.js} +1 -1
- package/src/client/dist/spa/assets/render-chat-markdown-DxEHr3lW.js +60 -0
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-D_RRiKBh.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-CO3Uuszk.js → tsMode-B6S4PLWH.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-BxouTBRr.js → typescript-Ca8AEX3t.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-DnSuDqo2.js +1 -0
- package/src/client/dist/spa/assets/use-id-BCnfiBjU.js +1 -0
- package/src/client/dist/spa/assets/use-onboarding-B3l7mx48.css +1 -0
- package/src/client/dist/spa/assets/use-onboarding-CNeLPDtv.js +2 -0
- package/src/client/dist/spa/assets/use-quasar-k24tGxE-.js +1 -0
- package/src/client/dist/spa/assets/{vue-i18n-cyuEW5NY.js → vue-i18n-Cq-KgjJC.js} +2 -2
- package/src/client/dist/spa/assets/{xml-W_9Wtmsi.js → xml-CsKo4k8C.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-ByUJ6m8w.js → yaml-X5yKmi6z.js} +1 -1
- package/src/client/dist/spa/index.html +13 -13
- package/src/client/dist/spa/assets/ActivityFeed-LlMDs201.js +0 -8
- package/src/client/dist/spa/assets/CreatePage-ZyBHUbl0.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-kLDqaPDs.js +0 -2
- package/src/client/dist/spa/assets/HealthPage-C3wPvMBd.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-BQc7MqCn.js +0 -37
- package/src/client/dist/spa/assets/QBtn-CLU6snCm.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-BFqbf9C7.js +0 -1
- package/src/client/dist/spa/assets/QIcon-0rjEivgj.js +0 -1
- package/src/client/dist/spa/assets/QInput-Ciqjq5-e.js +0 -1
- package/src/client/dist/spa/assets/QItemSection-DZnyqPM3.js +0 -1
- package/src/client/dist/spa/assets/QList-MfhZa-uv.js +0 -1
- package/src/client/dist/spa/assets/QMenu-Cx3v9_6z.js +0 -1
- package/src/client/dist/spa/assets/QPage-CGYPttdA.js +0 -1
- package/src/client/dist/spa/assets/QRadio-BEsMlsx1.js +0 -1
- package/src/client/dist/spa/assets/QSpace-BrtkvWzZ.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-DdpeVKGV.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-DifF1k07.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-DfNUJb99.js +0 -9
- package/src/client/dist/spa/assets/WorkspacePage-B-E0Ry6O.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-B4YnZ6re.css +0 -1
- package/src/client/dist/spa/assets/build-path-tree-DOtGuDWX.js +0 -1
- package/src/client/dist/spa/assets/documents-qOarUoMj.js +0 -1
- package/src/client/dist/spa/assets/engineFeatures-DYvwpYlV.js +0 -1
- package/src/client/dist/spa/assets/expand-template-BBEWjwKv.js +0 -1
- package/src/client/dist/spa/assets/i18n-CpmhEjSj.js +0 -1
- package/src/client/dist/spa/assets/index-DRHsozfK.js +0 -2
- package/src/client/dist/spa/assets/notifications-BzyK_kYE.js +0 -1
- package/src/client/dist/spa/assets/permissionModes-B1595Yfm.js +0 -1
- package/src/client/dist/spa/assets/project-color-d_h7ZYoM.js +0 -1
- package/src/client/dist/spa/assets/purify.es-DfpOsee9.js +0 -60
- package/src/client/dist/spa/assets/render-chat-markdown-CeI636ZV.js +0 -1
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-DPcTPMmX.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-DzLCp4E3.js +0 -1
- package/src/client/dist/spa/assets/use-id-BQW6DfJU.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-C5gKpYwL.js +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Kōbō are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
|
+
section — the in-app "What's new" dialog reads this file.
|
|
6
|
+
|
|
7
|
+
## 1.7.15
|
|
8
|
+
|
|
9
|
+
- docs: document new settings and features
|
|
10
|
+
- build(release): generate changelog section in version bump
|
|
11
|
+
- feat(onboarding): guided tour and what's-new dialog
|
|
12
|
+
- feat(settings): scripts, branch prefixes and project cards
|
|
13
|
+
- feat(workspaces): bulk-delete archived workspaces and fix flat sort
|
|
14
|
+
- feat(create): per-project task prompt template
|
|
15
|
+
- feat(health): show schema and settings migration versions
|
|
16
|
+
- feat(chat): dedicated script cards in the conversation feed
|
|
17
|
+
- feat(chat): @-mention file autocomplete with fuzzy matching
|
|
18
|
+
- feat(export): CSV export of workspace session events
|
|
19
|
+
- feat(server): lifecycle scripts, bulk delete and migration safety
|
|
20
|
+
|
|
21
|
+
## 1.7.14
|
|
22
|
+
|
|
23
|
+
- Show the Kōbō version in the Health page Environment card.
|
|
24
|
+
- Document the `SERVER_PORT` / `PORT` overrides and fix the default port.
|
|
25
|
+
- Split the configuration reference into a dedicated `CONFIGURATION.md`.
|
package/README.md
CHANGED
|
@@ -15,12 +15,14 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
15
15
|
|
|
16
16
|
- **Isolated worktrees** — each workspace is a dedicated git worktree on its own branch; parallel sessions never collide.
|
|
17
17
|
- **Two agent engines** — Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
|
|
18
|
-
- **Live chat** — streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback.
|
|
18
|
+
- **Live chat** — streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback; `/` autocompletes skills & commands and `@` fuzzy-autocompletes worktree file paths; every workspace's session events are exportable to CSV.
|
|
19
19
|
- **Task tracking** — per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
|
|
20
20
|
- **Git panel** — Monaco-based diff viewer, inline conflict resolution, `Sync` / `Push` / `Open PR` wired to the `gh` CLI.
|
|
21
21
|
- **Auto-loop** — opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
|
|
22
22
|
- **Quota-aware** — 5-hour / 7-day Claude usage and Codex rate-limit buckets in the footer; sessions auto-resume after a rate-limit reset.
|
|
23
|
-
- **
|
|
23
|
+
- **Scheduled wakeups** — the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool; Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
|
|
24
|
+
- **Cron schedules** — recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`); each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
|
|
25
|
+
- **Lifecycle scripts** — shell scripts run automatically at key moments: **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configured globally or per project, with their output streamed into the chat.
|
|
24
26
|
- **Optional integrations** — Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
|
|
25
27
|
|
|
26
28
|
## Quick start
|
|
@@ -63,7 +65,7 @@ The most common knobs:
|
|
|
63
65
|
| `NOTION_API_TOKEN` | — | Notion integration token |
|
|
64
66
|
| `OPENAI_API_KEY` | — | Codex engine credential (alternative to `codex login`) |
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime — per-project values inherit from the global ones when left empty.
|
|
67
69
|
|
|
68
70
|
The full reference — every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup — is in [`CONFIGURATION.md`](./CONFIGURATION.md).
|
|
69
71
|
|
|
@@ -332,6 +332,39 @@ export function runMigrations(db) {
|
|
|
332
332
|
db.prepare('INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)').run(m.version, m.name, new Date().toISOString());
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
|
+
// Soft downgrade guard: if the database carries a migration version this
|
|
336
|
+
// build doesn't know about, it was migrated by a newer Kōbō. Forward-only
|
|
337
|
+
// migrations have no `down` step — warn loudly but let the app continue.
|
|
338
|
+
const maxVersion = db.prepare('SELECT MAX(version) AS v FROM schema_migrations').get().v ?? 0;
|
|
339
|
+
if (maxVersion > SCHEMA_VERSION) {
|
|
340
|
+
console.warn(`[kobo] ⚠ Database schema version (${maxVersion}) is higher than this Kōbō build supports ` +
|
|
341
|
+
`(${SCHEMA_VERSION}). The database was migrated by a newer version of Kōbō — running an older ` +
|
|
342
|
+
`build may cause errors. Update Kōbō, or restore a pre-downgrade backup from the Kōbō home directory.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Return the versions of registered migrations not yet applied to `db`, without
|
|
347
|
+
* applying them. Used to decide whether a pre-migration backup is warranted.
|
|
348
|
+
* Returns `[]` for a fresh / uninitialised database — there is no data at risk.
|
|
349
|
+
*/
|
|
350
|
+
export function getPendingMigrations(db) {
|
|
351
|
+
const tableExists = (name) => db.prepare("SELECT count(*) AS c FROM sqlite_master WHERE type='table' AND name=?").get(name).c > 0;
|
|
352
|
+
let applied;
|
|
353
|
+
if (tableExists('schema_migrations')) {
|
|
354
|
+
applied = new Set(db.prepare('SELECT version FROM schema_migrations').all().map((r) => r.version));
|
|
355
|
+
}
|
|
356
|
+
else if (tableExists('schema_version')) {
|
|
357
|
+
// Legacy single-row table: every version up to the stored one is applied.
|
|
358
|
+
const legacy = db.prepare('SELECT version FROM schema_version LIMIT 1').get()?.version ?? 0;
|
|
359
|
+
applied = new Set();
|
|
360
|
+
for (let v = 1; v <= legacy; v++)
|
|
361
|
+
applied.add(v);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// Fresh / uninitialised DB — initSchema bootstraps it, nothing to back up.
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
return migrations.filter((m) => !applied.has(m.version)).map((m) => m.version);
|
|
335
368
|
}
|
|
336
369
|
/** Return the full migration history (for diagnostics / admin UI). */
|
|
337
370
|
export function getMigrationHistory(db) {
|
package/dist/server/index.js
CHANGED
|
@@ -5,10 +5,13 @@ import { serve } from '@hono/node-server';
|
|
|
5
5
|
import { Hono } from 'hono';
|
|
6
6
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
7
7
|
import { closeDb, getDb } from './db/index.js';
|
|
8
|
-
import { runMigrations } from './db/migrations.js';
|
|
8
|
+
import { getPendingMigrations, runMigrations } from './db/migrations.js';
|
|
9
|
+
import changelogRouter from './routes/changelog.js';
|
|
9
10
|
import devServerRouter from './routes/dev-server.js';
|
|
10
11
|
import documentsRouter from './routes/documents.js';
|
|
11
12
|
import { enginesRouter } from './routes/engines.js';
|
|
13
|
+
import exportRouter from './routes/export.js';
|
|
14
|
+
import fsRouter from './routes/fs.js';
|
|
12
15
|
import gitRouter from './routes/git.js';
|
|
13
16
|
import healthRouter from './routes/health.js';
|
|
14
17
|
import imagesRouter from './routes/images.js';
|
|
@@ -25,7 +28,7 @@ import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb,
|
|
|
25
28
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
26
29
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
27
30
|
import * as cronService from './services/cron-service.js';
|
|
28
|
-
import { createDailyDbBackupIfNeeded } from './services/db-backup-service.js';
|
|
31
|
+
import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
|
|
29
32
|
import { startDevServer, stopDevServer } from './services/dev-server-service.js';
|
|
30
33
|
import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
|
|
31
34
|
import * as quotaBackoffService from './services/quota-backoff-service.js';
|
|
@@ -39,6 +42,19 @@ import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/
|
|
|
39
42
|
console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
|
|
40
43
|
// Initialize DB + run migrations
|
|
41
44
|
const db = getDb();
|
|
45
|
+
// Pre-migration backup: snapshot the DB before applying any pending schema
|
|
46
|
+
// migration so a botched upgrade can be rolled back manually. Best-effort —
|
|
47
|
+
// a backup failure must not block boot (the daily backup is a second net).
|
|
48
|
+
try {
|
|
49
|
+
const pending = getPendingMigrations(db);
|
|
50
|
+
if (pending.length > 0) {
|
|
51
|
+
const result = await createPreMigrationBackup(db, getDbPath(), `v${pending[pending.length - 1]}`);
|
|
52
|
+
console.log(`[kobo] Pre-migration backup before applying ${pending.length} migration(s): ${result.created}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error('[kobo] Pre-migration backup failed (continuing — daily backup remains as fallback):', err);
|
|
57
|
+
}
|
|
42
58
|
runMigrations(db);
|
|
43
59
|
// Daily DB backup (best-effort, fire-and-forget — never blocks boot).
|
|
44
60
|
// Creates a WAL-safe snapshot alongside kobo.db if no backup exists in the
|
|
@@ -75,11 +91,14 @@ app.route('/api/workspaces', imagesRouter);
|
|
|
75
91
|
app.route('/api/notion', notionRouter);
|
|
76
92
|
app.route('/api/sentry', sentryRouter);
|
|
77
93
|
app.route('/api/git', gitRouter);
|
|
94
|
+
app.route('/api/fs', fsRouter);
|
|
95
|
+
app.route('/api/changelog', changelogRouter);
|
|
78
96
|
app.route('/api/settings', settingsRouter);
|
|
79
97
|
app.route('/api/dev-server', devServerRouter);
|
|
80
98
|
app.route('/api/templates', templatesRouter);
|
|
81
99
|
app.route('/api/usage', usageRoutes);
|
|
82
100
|
app.route('/api/workspaces', documentsRouter);
|
|
101
|
+
app.route('/api/workspaces', exportRouter);
|
|
83
102
|
app.route('/api/search', searchRouter);
|
|
84
103
|
app.route('/api/health', healthRouter);
|
|
85
104
|
app.route('/api/engines', enginesRouter);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { getChangelogPath, getPackageVersion } from '../utils/paths.js';
|
|
4
|
+
/** Hono sub-router for the in-app "What's new" dialog. */
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
/**
|
|
7
|
+
* Parse a Keep-a-Changelog markdown file into ordered version sections. Each
|
|
8
|
+
* `## <version>` heading starts a new entry; everything until the next heading
|
|
9
|
+
* is its notes. A leading `v` on the version is stripped.
|
|
10
|
+
*/
|
|
11
|
+
export function parseChangelog(markdown) {
|
|
12
|
+
const entries = [];
|
|
13
|
+
let current = null;
|
|
14
|
+
for (const line of markdown.split('\n')) {
|
|
15
|
+
const heading = line.match(/^##\s+v?(\d+\.\d+\.\d+[\w.-]*)\s*$/);
|
|
16
|
+
if (heading) {
|
|
17
|
+
if (current)
|
|
18
|
+
entries.push({ version: current.version, notes: current.lines.join('\n').trim() });
|
|
19
|
+
current = { version: heading[1], lines: [] };
|
|
20
|
+
}
|
|
21
|
+
else if (current) {
|
|
22
|
+
current.lines.push(line);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (current)
|
|
26
|
+
entries.push({ version: current.version, notes: current.lines.join('\n').trim() });
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
// GET /api/changelog — current app version + parsed CHANGELOG.md sections,
|
|
30
|
+
// powering the "What's new" dialog shown after an update.
|
|
31
|
+
app.get('/', (c) => {
|
|
32
|
+
try {
|
|
33
|
+
let versions = [];
|
|
34
|
+
try {
|
|
35
|
+
versions = parseChangelog(fs.readFileSync(getChangelogPath(), 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// No CHANGELOG.md shipped (or unreadable) — degrade to an empty list.
|
|
39
|
+
}
|
|
40
|
+
return c.json({ currentVersion: getPackageVersion(), versions });
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
return c.json({ error: message }, 500);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
export default app;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { getDb } from '../db/index.js';
|
|
3
|
+
import * as workspaceService from '../services/workspace-service.js';
|
|
4
|
+
/** Hono sub-router for workspace data exports (mounted on /api/workspaces). */
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
/** Quote a CSV cell when it contains a delimiter, quote, or newline (RFC 4180). */
|
|
7
|
+
function csvCell(value) {
|
|
8
|
+
return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
9
|
+
}
|
|
10
|
+
function csvRow(values) {
|
|
11
|
+
return values.map(csvCell).join(',');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Best-effort extraction of a human-readable text from an event payload.
|
|
15
|
+
* Different event types carry their text under different keys; the full
|
|
16
|
+
* payload is exported alongside, so this is a convenience column only.
|
|
17
|
+
*/
|
|
18
|
+
function extractText(payload) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(payload);
|
|
21
|
+
if (!parsed || typeof parsed !== 'object')
|
|
22
|
+
return '';
|
|
23
|
+
const obj = parsed;
|
|
24
|
+
for (const key of ['text', 'content', 'message']) {
|
|
25
|
+
const v = obj[key];
|
|
26
|
+
if (typeof v === 'string')
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* malformed payload — leave the text column empty */
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
/** Filesystem-safe slug for the download filename. */
|
|
36
|
+
function slugify(name) {
|
|
37
|
+
const slug = name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
40
|
+
.replace(/^-+|-+$/g, '');
|
|
41
|
+
return slug || 'workspace';
|
|
42
|
+
}
|
|
43
|
+
// GET /api/workspaces/:id/events.csv — export every ws_event of the workspace
|
|
44
|
+
// (all sessions, chronological) as a CSV file.
|
|
45
|
+
app.get('/:id/events.csv', (c) => {
|
|
46
|
+
try {
|
|
47
|
+
const id = c.req.param('id');
|
|
48
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
49
|
+
if (!workspace) {
|
|
50
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
51
|
+
}
|
|
52
|
+
const rows = getDb()
|
|
53
|
+
.prepare('SELECT session_id, type, payload, created_at FROM ws_events WHERE workspace_id = ? ORDER BY rowid ASC')
|
|
54
|
+
.all(id);
|
|
55
|
+
const lines = [csvRow(['created_at', 'session_id', 'type', 'text', 'payload'])];
|
|
56
|
+
for (const r of rows) {
|
|
57
|
+
lines.push(csvRow([r.created_at, r.session_id ?? '', r.type, extractText(r.payload), r.payload]));
|
|
58
|
+
}
|
|
59
|
+
// Leading BOM so Excel reads UTF-8 correctly; CRLF line endings per RFC 4180.
|
|
60
|
+
const csv = `${lines.join('\r\n')}\r\n`;
|
|
61
|
+
return c.body(csv, 200, {
|
|
62
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
63
|
+
'Content-Disposition': `attachment; filename="${slugify(workspace.name)}-events.csv"`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return c.json({ error: message }, 500);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
export default app;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
/** Hono sub-router for local filesystem browsing (project folder picker). */
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
// GET /api/fs/list-dirs?path=<dir> — list the immediate subdirectories of a
|
|
8
|
+
// directory. Defaults to the user's home directory when `path` is omitted.
|
|
9
|
+
// Local single-user dev tool: the user browses their own filesystem.
|
|
10
|
+
app.get('/list-dirs', (c) => {
|
|
11
|
+
try {
|
|
12
|
+
const requested = c.req.query('path');
|
|
13
|
+
const target = path.resolve(requested?.trim() ? requested : os.homedir());
|
|
14
|
+
let stat;
|
|
15
|
+
try {
|
|
16
|
+
stat = fs.statSync(target);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return c.json({ error: `Directory not found: ${target}` }, 404);
|
|
20
|
+
}
|
|
21
|
+
if (!stat.isDirectory()) {
|
|
22
|
+
return c.json({ error: `Not a directory: ${target}` }, 400);
|
|
23
|
+
}
|
|
24
|
+
let dirents;
|
|
25
|
+
try {
|
|
26
|
+
dirents = fs.readdirSync(target, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
return c.json({ error: `Cannot read directory: ${message}` }, 403);
|
|
31
|
+
}
|
|
32
|
+
// Directories only, hidden ones excluded, sorted case-insensitively.
|
|
33
|
+
const entries = dirents
|
|
34
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.'))
|
|
35
|
+
.map((d) => ({ name: d.name, path: path.join(target, d.name) }))
|
|
36
|
+
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
37
|
+
const parent = path.dirname(target);
|
|
38
|
+
return c.json({
|
|
39
|
+
path: target,
|
|
40
|
+
parent: parent === target ? null : parent,
|
|
41
|
+
entries,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return c.json({ error: message }, 500);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
export default app;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { getDb } from '../db/index.js';
|
|
3
3
|
import { listOrphanWorktrees } from '../services/worktree-service.js';
|
|
4
|
-
import { listBranches, listRemoteBranches } from '../utils/git-ops.js';
|
|
4
|
+
import { listBranches, listRemoteBranches, listWorktreeFiles } from '../utils/git-ops.js';
|
|
5
5
|
/** Hono sub-router for git-related endpoints (branch listing). */
|
|
6
6
|
const app = new Hono();
|
|
7
7
|
// GET /api/git/branches?path=<repoPath> — list branches for a repo
|
|
@@ -20,6 +20,21 @@ app.get('/branches', (c) => {
|
|
|
20
20
|
return c.json({ error: message }, 500);
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
|
+
// GET /api/git/files?path=<worktreePath> — list a worktree's files (tracked +
|
|
24
|
+
// untracked-but-not-ignored) for the chat's `@file` autocomplete.
|
|
25
|
+
app.get('/files', (c) => {
|
|
26
|
+
try {
|
|
27
|
+
const repoPath = c.req.query('path');
|
|
28
|
+
if (!repoPath) {
|
|
29
|
+
return c.json({ error: 'Missing required query parameter: path' }, 400);
|
|
30
|
+
}
|
|
31
|
+
return c.json({ files: listWorktreeFiles(repoPath) });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
return c.json({ error: message }, 500);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
23
38
|
// GET /api/git/orphan-worktrees?projectPath=<path> — list worktrees of a
|
|
24
39
|
// project that are NOT attached to any Kōbō workspace yet.
|
|
25
40
|
app.get('/orphan-worktrees', (c) => {
|
|
@@ -9,6 +9,7 @@ import { getDb } from '../db/index.js';
|
|
|
9
9
|
import { migrationGuard } from '../middleware/migration-guard.js';
|
|
10
10
|
import { listEngines } from '../services/agent/engines/registry.js';
|
|
11
11
|
import * as agentManager from '../services/agent/orchestrator.js';
|
|
12
|
+
import * as archiveScriptService from '../services/archive-script-service.js';
|
|
12
13
|
import * as autoLoopService from '../services/auto-loop-service.js';
|
|
13
14
|
import * as cronService from '../services/cron-service.js';
|
|
14
15
|
import * as devServerService from '../services/dev-server-service.js';
|
|
@@ -1694,6 +1695,8 @@ app.post('/:id/archive', migrationGuard, (c) => {
|
|
|
1694
1695
|
}
|
|
1695
1696
|
const updated = workspaceService.archiveWorkspace(id);
|
|
1696
1697
|
wsService.emitEphemeral(id, 'workspace:archived', { workspace: updated });
|
|
1698
|
+
// Run the project's archive script (best-effort — never blocks the archive).
|
|
1699
|
+
archiveScriptService.onWorkspaceArchived(id);
|
|
1697
1700
|
return c.json(updated);
|
|
1698
1701
|
}
|
|
1699
1702
|
catch (err) {
|
|
@@ -1721,86 +1724,126 @@ app.post('/:id/unarchive', migrationGuard, (c) => {
|
|
|
1721
1724
|
return c.json({ error: message }, 500);
|
|
1722
1725
|
}
|
|
1723
1726
|
});
|
|
1724
|
-
//
|
|
1725
|
-
|
|
1727
|
+
// Shared teardown for a single workspace: stops the agent, destroys the
|
|
1728
|
+
// terminal, removes the owned worktree, optionally deletes local/remote
|
|
1729
|
+
// branches, then deletes the DB row (cascades to tasks/sessions/events).
|
|
1730
|
+
// Every side-effect is best-effort — failures are collected as warnings
|
|
1731
|
+
// rather than thrown, so a bulk delete never aborts mid-batch. Returns the
|
|
1732
|
+
// list of user-facing warning messages (empty when everything was clean).
|
|
1733
|
+
function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
1734
|
+
// Stop agent if running (best-effort)
|
|
1726
1735
|
try {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
//
|
|
1736
|
+
agentManager.stopAgent(workspace.id);
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
// Agent may not be running — ignore
|
|
1740
|
+
}
|
|
1741
|
+
try {
|
|
1742
|
+
terminalService.destroyTerminal(workspace.id);
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
// Terminal may not exist — ignore
|
|
1746
|
+
}
|
|
1747
|
+
// Collected best-effort warnings: the DB deletion always proceeds, but
|
|
1748
|
+
// side-effects (worktree, local/remote branches) can fail independently.
|
|
1749
|
+
// We surface a user-friendly message per failure so the UI can show a
|
|
1750
|
+
// sticky toast with a copy-pasteable recovery command — common case:
|
|
1751
|
+
// Docker leaves root-owned files inside the worktree, git worktree
|
|
1752
|
+
// remove fails with EACCES.
|
|
1753
|
+
const warnings = [];
|
|
1754
|
+
// Remove worktree (only if owned — for attached external worktrees we
|
|
1755
|
+
// never created the dir, so we must not delete it on the user's behalf).
|
|
1756
|
+
const worktreePath = workspace.worktreePath;
|
|
1757
|
+
if (workspace.worktreeOwned) {
|
|
1737
1758
|
try {
|
|
1738
|
-
|
|
1759
|
+
worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
1739
1760
|
}
|
|
1740
|
-
catch {
|
|
1741
|
-
|
|
1761
|
+
catch (err) {
|
|
1762
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1763
|
+
console.error(`[workspaces] Failed to remove worktree: ${message}`);
|
|
1764
|
+
warnings.push(`Failed to remove worktree directory '${worktreePath}'. The git entry may still reference it. ` +
|
|
1765
|
+
`Fix manually:\n` +
|
|
1766
|
+
` sudo rm -rf '${worktreePath}'\n` +
|
|
1767
|
+
` cd '${workspace.projectPath}' && git worktree prune\n` +
|
|
1768
|
+
`Reason: ${message}`);
|
|
1742
1769
|
}
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
console.log(`[workspaces] keeping reused worktree on delete: ${worktreePath}`);
|
|
1773
|
+
}
|
|
1774
|
+
// Delete local branch if requested
|
|
1775
|
+
if (opts.deleteLocalBranch) {
|
|
1743
1776
|
try {
|
|
1744
|
-
|
|
1777
|
+
gitOps.deleteLocalBranch(workspace.projectPath, workspace.workingBranch);
|
|
1745
1778
|
}
|
|
1746
|
-
catch {
|
|
1747
|
-
|
|
1779
|
+
catch (err) {
|
|
1780
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1781
|
+
console.error(`[workspaces] Failed to delete local branch: ${message}`);
|
|
1782
|
+
warnings.push(`Failed to delete local branch '${workspace.workingBranch}'. Fix manually:\n` +
|
|
1783
|
+
` cd '${workspace.projectPath}' && git branch -D '${workspace.workingBranch}'\n` +
|
|
1784
|
+
`Reason: ${message}`);
|
|
1748
1785
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
// remove fails with EACCES.
|
|
1755
|
-
const warnings = [];
|
|
1756
|
-
// Remove worktree (only if owned — for attached external worktrees we
|
|
1757
|
-
// never created the dir, so we must not delete it on the user's behalf).
|
|
1758
|
-
const worktreePath = workspace.worktreePath;
|
|
1759
|
-
if (workspace.worktreeOwned) {
|
|
1760
|
-
try {
|
|
1761
|
-
worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
1762
|
-
}
|
|
1763
|
-
catch (err) {
|
|
1764
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1765
|
-
console.error(`[workspaces] Failed to remove worktree: ${message}`);
|
|
1766
|
-
warnings.push(`Failed to remove worktree directory '${worktreePath}'. The git entry may still reference it. ` +
|
|
1767
|
-
`Fix manually:\n` +
|
|
1768
|
-
` sudo rm -rf '${worktreePath}'\n` +
|
|
1769
|
-
` cd '${workspace.projectPath}' && git worktree prune\n` +
|
|
1770
|
-
`Reason: ${message}`);
|
|
1771
|
-
}
|
|
1786
|
+
}
|
|
1787
|
+
// Delete remote branch if requested
|
|
1788
|
+
if (opts.deleteRemoteBranch) {
|
|
1789
|
+
try {
|
|
1790
|
+
gitOps.deleteRemoteBranch(workspace.projectPath, workspace.workingBranch);
|
|
1772
1791
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1792
|
+
catch (err) {
|
|
1793
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1794
|
+
console.error(`[workspaces] Failed to delete remote branch: ${message}`);
|
|
1795
|
+
warnings.push(`Failed to delete remote branch '${workspace.workingBranch}'. Fix manually:\n` +
|
|
1796
|
+
` cd '${workspace.projectPath}' && git push origin --delete '${workspace.workingBranch}'\n` +
|
|
1797
|
+
`Reason: ${message}`);
|
|
1775
1798
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1799
|
+
}
|
|
1800
|
+
// Delete workspace from DB (cascades to tasks, sessions, events)
|
|
1801
|
+
workspaceService.deleteWorkspace(workspace.id);
|
|
1802
|
+
return warnings;
|
|
1803
|
+
}
|
|
1804
|
+
// DELETE /api/workspaces/archived — bulk-delete every archived workspace.
|
|
1805
|
+
// Must be declared BEFORE `DELETE /:id` or the dynamic segment captures it.
|
|
1806
|
+
// Each workspace teardown is isolated: an error on one is swallowed into the
|
|
1807
|
+
// warnings list and never aborts the batch.
|
|
1808
|
+
app.delete('/archived', migrationGuard, async (c) => {
|
|
1809
|
+
try {
|
|
1810
|
+
const body = await c.req
|
|
1811
|
+
.json()
|
|
1812
|
+
.catch(() => ({}));
|
|
1813
|
+
const archived = workspaceService.listArchivedWorkspaces();
|
|
1814
|
+
const warnings = [];
|
|
1815
|
+
let deleted = 0;
|
|
1816
|
+
for (const workspace of archived) {
|
|
1778
1817
|
try {
|
|
1779
|
-
|
|
1818
|
+
warnings.push(...deleteWorkspaceWithSideEffects(workspace, body));
|
|
1819
|
+
deleted++;
|
|
1780
1820
|
}
|
|
1781
1821
|
catch (err) {
|
|
1782
1822
|
const message = err instanceof Error ? err.message : String(err);
|
|
1783
|
-
console.error(`[workspaces] Failed to delete
|
|
1784
|
-
warnings.push(`Failed to delete
|
|
1785
|
-
` cd '${workspace.projectPath}' && git branch -D '${workspace.workingBranch}'\n` +
|
|
1786
|
-
`Reason: ${message}`);
|
|
1823
|
+
console.error(`[workspaces] Failed to delete archived workspace '${workspace.id}': ${message}`);
|
|
1824
|
+
warnings.push(`Failed to delete workspace '${workspace.name}': ${message}`);
|
|
1787
1825
|
}
|
|
1788
1826
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1827
|
+
return c.json({ ok: true, deleted, warnings }, 200);
|
|
1828
|
+
}
|
|
1829
|
+
catch (err) {
|
|
1830
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1831
|
+
return c.json({ error: message }, 500);
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
// DELETE /api/workspaces/:id — delete workspace
|
|
1835
|
+
app.delete('/:id', migrationGuard, async (c) => {
|
|
1836
|
+
try {
|
|
1837
|
+
const id = c.req.param('id');
|
|
1838
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1839
|
+
if (!workspace) {
|
|
1840
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1801
1841
|
}
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1842
|
+
// Parse optional body for branch deletion options
|
|
1843
|
+
const body = await c.req
|
|
1844
|
+
.json()
|
|
1845
|
+
.catch(() => ({}));
|
|
1846
|
+
const warnings = deleteWorkspaceWithSideEffects(workspace, body);
|
|
1804
1847
|
// When everything worked cleanly we keep the legacy 204 response so
|
|
1805
1848
|
// existing clients aren't surprised by a JSON body. Warnings promote the
|
|
1806
1849
|
// response to 200 so the body is readable.
|
|
@@ -4,6 +4,7 @@ import { getDb } from '../../db/index.js';
|
|
|
4
4
|
import { ensureKoboHome, getCompiledMcpServerPath, getDbPath, getKoboHome, getMcpServerSourcePath, getSettingsPath, getSkillsPath, } from '../../utils/paths.js';
|
|
5
5
|
import { unregisterProcess } from '../../utils/process-tracker.js';
|
|
6
6
|
import * as autoLoopService from '../auto-loop-service.js';
|
|
7
|
+
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
7
8
|
import * as cronService from '../cron-service.js';
|
|
8
9
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
9
10
|
import { getEffectiveSettings } from '../settings-service.js';
|
|
@@ -308,6 +309,10 @@ function readEffectiveSettingsSafe(projectPath) {
|
|
|
308
309
|
sourceBranch: 'main',
|
|
309
310
|
devServer: null,
|
|
310
311
|
setupScript: '',
|
|
312
|
+
cleanupScript: '',
|
|
313
|
+
cleanupScriptMode: 'no-tasks',
|
|
314
|
+
cleanupScriptOnlyOnChanges: false,
|
|
315
|
+
archiveScript: '',
|
|
311
316
|
notionStatusProperty: '',
|
|
312
317
|
notionInProgressStatus: '',
|
|
313
318
|
};
|
|
@@ -531,7 +536,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
531
536
|
// cleared, next iteration will start fresh) — report 'completed' to
|
|
532
537
|
// auto-loop so it continues.
|
|
533
538
|
const effectiveReason = isResumeFailed ? 'completed' : ev.reason;
|
|
539
|
+
// Capture the auto-loop flag BEFORE autoLoopService.onSessionEnded —
|
|
540
|
+
// disable() clears it, and the cleanup hook needs to know whether this was
|
|
541
|
+
// a mid-loop session (never cleans) or a standalone one.
|
|
542
|
+
const wasAutoLoop = autoLoopService.getStatus(workspaceId).auto_loop;
|
|
534
543
|
autoLoopService.onSessionEnded(workspaceId, effectiveReason, delta);
|
|
544
|
+
cleanupScriptService.onSessionEnded(workspaceId, effectiveReason, { wasAutoLoop });
|
|
535
545
|
}
|
|
536
546
|
if (ev.kind === 'session:user-input-requested') {
|
|
537
547
|
if (ev.requestKind === 'question') {
|