@loicngr/kobo 1.11.39 → 1.11.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +101 -32
- package/CHANGELOG.md +8 -0
- package/README.md +6 -4
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/images.js +65 -5
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +250 -248
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/attachment-service.js +122 -0
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/image-service.js +16 -7
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/attachments.js +58 -0
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DTIe6O9o.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BWeu0YeI.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-o_EHlTwV.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-CGi88k6z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DbY5gBv8.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CIo_gesD.js +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DEcf48KI.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-D9vcB91W.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-Ca0_CxSl.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DCmB2R5i.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-_q3vy_xn.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Duwnq3cc.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-BeiZIHVA.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-Fj3M9QGD.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DtSUuK8t.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-DwkX8eQ6.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BlZDd7TK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGi1yRxU.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-DnO-ImWE.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-vz6jN1DM.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-IdPIGp2_.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DTJqxVp9.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-Bj2GGyhU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BBaGZlwv.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Dy0spsfi.js +4 -0
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-BFj5wwbS.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-C-HSiySk.js} +1 -1
- package/src/client/dist/pwa/assets/de-BdLe7apJ.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-C2Xnjh9Z.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-BbKfnNmv.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-BM7fdaaL.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-b74YRODT.js +1 -0
- package/src/client/dist/pwa/assets/es-C1LM-3aq.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-DiDWiIkx.js +1 -0
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-CnbUxAmw.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-B2uUIbAY.js} +1 -1
- package/src/client/dist/pwa/assets/fr-KCK6r5ga.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-Cu1vGY0x.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-CETuqLyz.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-BTNpr39P.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BkUirOzl.js +1 -0
- package/src/client/dist/pwa/assets/index-mvh5w1cM.js +20 -0
- package/src/client/dist/pwa/assets/it-BM6t0IGU.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C7Ncb6V7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-D0hUdg4x.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-BK5H1Xst.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-BBW6JNgz.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-1LZrAlY4.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-CVGS__6a.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-DIKLxr8F.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-B02zuH1j.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-C3otN1Vk.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-BUcW_eZA.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DEeUVvnp.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-CG6cL8ya.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-Ct_T5TPN.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-CSQAg5UC.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-_6HwJ_Rp.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-B8WWsWEI.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-BSB4-DXR.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-CSGKph85.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +169 -9
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BAC6u9OP.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-B6xAlAaM.js +0 -1
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
package/AGENTS.md
CHANGED
|
@@ -4,9 +4,9 @@ Guidance for AI coding agents (Claude Code, Cursor, etc.) working on this reposi
|
|
|
4
4
|
|
|
5
5
|
## What this project is
|
|
6
6
|
|
|
7
|
-
**Kōbō** (工房, Japanese for "workshop") orchestrates
|
|
7
|
+
**Kōbō** (工房, Japanese for "workshop") orchestrates Claude Code and OpenAI Codex agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, agent sessions, optional dev server, optional Notion/Sentry context, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, review Git changes, and manage the lifecycle. A workspace can switch engines through a handoff to a fresh session while preserving its worktree and history.
|
|
8
8
|
|
|
9
|
-
Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback
|
|
9
|
+
Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback API/WS requests behind a shared token. Behind-a-reverse-proxy mode also requires the token for loopback API/WS requests; `/api/health` stays exempt. Host/Origin checks apply separately. No multi-tenant concerns.
|
|
10
10
|
|
|
11
11
|
## Tech stack
|
|
12
12
|
|
|
@@ -16,9 +16,9 @@ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the
|
|
|
16
16
|
|
|
17
17
|
**Database**: a single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production**, so every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
|
|
18
18
|
|
|
19
|
-
**Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
|
|
19
|
+
**Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`, and `workspace-templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
|
|
20
20
|
|
|
21
|
-
**Tests**: Vitest covers backend services/routes and frontend stores
|
|
21
|
+
**Tests**: Vitest covers backend services/routes and frontend stores, utilities, composables, and selected Vue components. The client suite uses Vue Test Utils and `happy-dom`; type-checking and manual smoke tests complement it. `npm test` runs the backend suite only; `npm run test:all` runs both suites. The three dependency trees (root, client, PWA) have separate lockfiles.
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
@@ -36,8 +36,12 @@ npm run dev:all # both concurrently
|
|
|
36
36
|
# Check & test
|
|
37
37
|
npm run lint # biome check (linting + formatting)
|
|
38
38
|
npx tsc --noEmit # type check backend (the project's primary quality gate)
|
|
39
|
-
npm
|
|
40
|
-
(cd src/client && npm
|
|
39
|
+
npm run typecheck:tests # type check backend test fixtures and tests
|
|
40
|
+
(cd src/client && npm run type-check) # vue-tsc: client + Vue components
|
|
41
|
+
npm test # backend vitest suite
|
|
42
|
+
(cd src/client && npm test) # client stores, utilities, composables and components
|
|
43
|
+
npm run test:all # backend + client suites
|
|
44
|
+
make ci # install, audit, lint, type checks, build, both suites
|
|
41
45
|
npm run test:watch # vitest in watch mode
|
|
42
46
|
|
|
43
47
|
# Build & run
|
|
@@ -56,15 +60,15 @@ src/
|
|
|
56
60
|
│ ├── db/
|
|
57
61
|
│ │ ├── index.ts # singleton getDb / closeDb
|
|
58
62
|
│ │ ├── schema.ts # initSchema — CREATE TABLE IF NOT EXISTS …
|
|
59
|
-
│ │ └── migrations.ts #
|
|
63
|
+
│ │ └── migrations.ts # append-only migration registry + transactional runner
|
|
60
64
|
│ ├── services/ # business logic — pure functions over db + external processes
|
|
61
65
|
│ │ ├── workspace-service.ts # workspaces + tasks + agent_sessions CRUD
|
|
62
66
|
│ │ ├── agent/ # agent engine abstraction (replaces former agent-manager.ts)
|
|
63
67
|
│ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota handling, watchdog, public API
|
|
64
68
|
│ │ │ ├── session-controller.ts # lifecycle wrapper around an AgentEngine instance
|
|
65
|
-
│ │ │ ├── event-router.ts #
|
|
69
|
+
│ │ │ ├── event-router.ts # persists/broadcasts normalized AgentEvent via WebSocket
|
|
66
70
|
│ │ │ └── engines/{claude-code,codex}/ # per-engine adapters implementing the AgentEngine contract
|
|
67
|
-
│ │ ├── forge/ #
|
|
71
|
+
│ │ ├── forge/ # github / gitlab / bitbucket-community / none, registry + auto-resolve
|
|
68
72
|
│ │ ├── change-source-branch-service.ts # re-target a workspace onto a new source branch (built-in cherry-pick or custom bash)
|
|
69
73
|
│ │ ├── git-stats-service.ts # pure compute of commit/ahead-behind/diff stats + forge availability for a workspace
|
|
70
74
|
│ │ ├── settings-defaults.ts # DEFAULT_* constants for opt-in settings (e.g. change-source-branch script)
|
|
@@ -83,7 +87,8 @@ src/
|
|
|
83
87
|
│ │ ├── workspaces.ts # /api/workspaces/* — the main surface
|
|
84
88
|
│ │ ├── templates.ts # /api/templates — prompt templates CRUD
|
|
85
89
|
│ │ ├── workspace-templates.ts # /api/workspace-templates - saved create-form presets
|
|
86
|
-
│ │ ├──
|
|
90
|
+
│ │ ├── documents.ts # workspace plans, specs and thoughts browser (read-only)
|
|
91
|
+
│ │ ├── mcp.ts # /api/mcp — external discovery and conversation tools
|
|
87
92
|
│ │ ├── dev-server.ts, git.ts, notion.ts, settings.ts
|
|
88
93
|
│ ├── utils/
|
|
89
94
|
│ │ ├── git-ops.ts # pushBranch, pullBranch, getCommitsBetween, delete{Local,Remote}Branch…
|
|
@@ -91,7 +96,7 @@ src/
|
|
|
91
96
|
├── client/ # Vue 3 + Quasar PWA
|
|
92
97
|
│ └── src/
|
|
93
98
|
│ ├── stores/ # pinia: workspace, websocket, settings, dev-server, templates
|
|
94
|
-
│ ├── components/ # WorkspaceList,
|
|
99
|
+
│ ├── components/ # WorkspaceList, AcceptancePanel, ChatInput, GitPanel, DocumentsPanel, WorkspaceAttentionLabels, HelpMenu…
|
|
95
100
|
│ ├── utils/ # expand-template, formatters, workspace-attention (CI failure / changes-requested derivation)…
|
|
96
101
|
│ ├── tours/ # declarative onboarding tours, one file per zone + registry
|
|
97
102
|
│ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
|
|
@@ -99,7 +104,8 @@ src/
|
|
|
99
104
|
├── mcp-server/ # standalone MCP server spawned per workspace
|
|
100
105
|
│ ├── kobo-tasks-server.ts # entrypoint, registers tools
|
|
101
106
|
│ └── kobo-tasks-handlers.ts # pure handlers (list_tasks, mark_task_done)
|
|
102
|
-
|
|
107
|
+
├── shared/ # model catalogues, prompts, dialogue contracts, shared utilities
|
|
108
|
+
└── __tests__/ # backend Vitest suites; client tests live under client/src/__tests__/
|
|
103
109
|
```
|
|
104
110
|
|
|
105
111
|
## Data model (SQLite)
|
|
@@ -110,7 +116,14 @@ src/
|
|
|
110
116
|
| `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
|
|
111
117
|
| `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
|
|
112
118
|
| `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
|
|
113
|
-
| `pending_wakeups` | one-row-per-workspace scheduler for the `
|
|
119
|
+
| `pending_wakeups` | one-row-per-workspace scheduler for the `schedule_wakeup` MCP tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
|
|
120
|
+
| `pending_quota_backoffs` | persisted quota retries and their attempt counts |
|
|
121
|
+
| `pending_crons` | recurring workspace schedules and their next run |
|
|
122
|
+
| `session_event_metrics` / `usage_snapshots` | session event counters and provider usage snapshots |
|
|
123
|
+
| `workspace_activity` | significant lifecycle/PR events for the absence digest |
|
|
124
|
+
| `search_*` | derived logical-message search index and indexing progress; see `search-schema.ts` |
|
|
125
|
+
| `mcp_message_requests` | durable external-message idempotency ledger; independent of event retention |
|
|
126
|
+
| `schema_migrations` | applied migration versions, names and timestamps |
|
|
114
127
|
| `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
|
|
115
128
|
| `workspace_permission_rules` | remembered per-workspace tool approvals, scoped to an exact operation or every invocation of a tool; CASCADE DELETE on workspace |
|
|
116
129
|
|
|
@@ -132,8 +145,8 @@ src/
|
|
|
132
145
|
|
|
133
146
|
### The two files and their roles
|
|
134
147
|
|
|
135
|
-
- **`src/server/db/schema.ts`**: `initSchema(db)`
|
|
136
|
-
- **`src/server/db/migrations.ts`**: `runMigrations(db)`
|
|
148
|
+
- **`src/server/db/schema.ts`**: `initSchema(db)` creates the current application schema for **fresh installs only**, including the search and MCP-message schemas through their helpers.
|
|
149
|
+
- **`src/server/db/migrations.ts`**: the ordered `migrations` registry contains `{ version, name, migrate(db) }` entries. `SCHEMA_VERSION` is derived from its last entry. `runMigrations(db)` records applied entries in `schema_migrations(version, name, applied_at)` and applies each pending entry in its own transaction. A fresh installation runs `initSchema` and records all entries as applied in one transaction. Legacy `schema_version` databases are converted to the history table automatically.
|
|
137
150
|
|
|
138
151
|
Both files must be kept in sync: after adding a migration, update `initSchema` so fresh installs get the same final shape without replaying migrations.
|
|
139
152
|
|
|
@@ -141,10 +154,10 @@ Both files must be kept in sync: after adding a migration, update `initSchema` s
|
|
|
141
154
|
|
|
142
155
|
Every feature that touches the schema:
|
|
143
156
|
|
|
144
|
-
1.
|
|
145
|
-
2.
|
|
157
|
+
1. Append an entry to `migrations` with the next version and a descriptive name. `SCHEMA_VERSION` updates automatically; do not replace its derived expression with a literal.
|
|
158
|
+
2. Implement the entry's `migrate(db)` using raw SQL, such as `ALTER TABLE workspaces ADD COLUMN new_field TEXT`. The runner owns the transaction and skips versions already recorded.
|
|
146
159
|
3. Update `initSchema` in `schema.ts` so the fresh-install shape matches (e.g. add the new column to the `CREATE TABLE` statement)
|
|
147
|
-
4.
|
|
160
|
+
4. Let the runner record the new entry in `schema_migrations`; do not manually update a legacy `schema_version` row.
|
|
148
161
|
5. Add a test in `src/__tests__/migrations.test.ts` that verifies:
|
|
149
162
|
- A database at the previous version can be upgraded without data loss
|
|
150
163
|
- The new version matches `SCHEMA_VERSION`
|
|
@@ -157,18 +170,31 @@ Every feature that touches the schema:
|
|
|
157
170
|
- **Always idempotent where possible.** Use `IF NOT EXISTS`, check for column existence before altering, etc. Prefer migrations that can be safely re-run.
|
|
158
171
|
- **`ALTER TABLE ADD COLUMN` is safe in SQLite** (even on large tables). For more invasive changes (rename, drop, change type), use the [12-step SQLite pattern](https://sqlite.org/lang_altertable.html#otheralter) within a transaction.
|
|
159
172
|
|
|
160
|
-
- **Run migrations on every backend start.** `
|
|
173
|
+
- **Run migrations on every backend start.** `src/server/index.ts` opens the connection with `getDb()`, checks for pending migrations, attempts a pre-migration backup, then calls `runMigrations(db)`. `getDb()` only opens/configures SQLite; it does not initialize or migrate the schema. Tests creating their own database must initialize it explicitly.
|
|
174
|
+
- **Settings migrations are separate.** The JSON settings migration versions in `settings-service.ts` are independent of SQLite's `SCHEMA_VERSION`.
|
|
161
175
|
- **Test upgrades, not just fresh installs.** The `migrations.test.ts` suite must exercise "old DB → new DB" paths.
|
|
162
176
|
|
|
163
177
|
## WebSocket protocol
|
|
164
178
|
|
|
179
|
+
### External MCP dialogue and update checks
|
|
180
|
+
|
|
181
|
+
`POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind the existing Host/Origin and network-token gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
|
|
182
|
+
|
|
183
|
+
Optional `send_workspace_message.idempotency_key` uses `mcp_message_requests` (migration v43). Reserve before dispatch, preserve fingerprints and terminal outcomes, and never automatically resend an unfinished request after restart. The user event and accepted receipt commit in one SQLite transaction; broadcast follows commit. Dispatch may precede persistence, so interrupted requests are explicitly `delivery_unknown`, not an exactly-once execution promise. The backend alone calls `reconcileMessageRequests` on startup. History retention does not delete the ledger; workspace deletion cascades it.
|
|
184
|
+
|
|
185
|
+
MCP messages and question answers include optional `source: { kind: 'mcp', clientName, transport }`; keep it across live, sync, history and conversation grouping paths without changing legacy `sender` behavior. Client names are display labels, not authorization identities. Unicode HTTP labels use URI encoding with `X-Kobo-Client-Name-Encoding: uri`; stdio forwards the initialization name or `KOBO_MCP_CLIENT_NAME` override.
|
|
186
|
+
|
|
187
|
+
`update-check-service.ts` owns the single immediate/every-20-minute npm release check. It is explicitly started/stopped by the backend, coalesces route/poller calls, and broadcasts ephemeral global `kobo:update-checked` events. The client update store also refreshes at reconnect. Do not reintroduce a per-tab registry poll or automatically install updates.
|
|
188
|
+
|
|
189
|
+
The explicit `npm run test:mcp:live` / `make test-mcp-live` command requires `KOBO_LIVE_ENGINE` and `KOBO_LIVE_MODEL`, uses real credentials and disposable data, and must stay outside `make ci`. Normal CI uses deterministic transport/service tests.
|
|
190
|
+
|
|
165
191
|
Clients subscribe to individual workspace ids. The server sends `WsEvent` objects:
|
|
166
192
|
|
|
167
193
|
```ts
|
|
168
194
|
{ id, workspaceId, type, payload, sessionId?, createdAt }
|
|
169
195
|
```
|
|
170
196
|
|
|
171
|
-
|
|
197
|
+
Agent engines emit a normalized `AgentEvent` union, carried by `agent:event`. Common outer types include `user:message`, `task:updated`, `devserver:status`, `workspace:status`, `workspace:archived`, `workspace:unarchived`, and `sync:response`. Legacy `agent:output` rows remain supported through content migration. Keep the backend union in `services/agent/engines/types.ts` and its client mirror in `client/src/types/agent-event.ts` synchronized.
|
|
172
198
|
|
|
173
199
|
Two emit flavors in `websocket-service.ts`:
|
|
174
200
|
- `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
|
|
@@ -178,7 +204,7 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
178
204
|
|
|
179
205
|
### Forge providers
|
|
180
206
|
|
|
181
|
-
`src/server/services/forge/` implements
|
|
207
|
+
`src/server/services/forge/` implements `ForgeProvider` for `github` (`gh`), `gitlab` (`glab`), `bitbucket-community` (`bkt`), and `none` (disables PR/MR features). `getForgeProvider(id)` resolves a provider; `resolveForge(projectPath)` first reads the explicit project setting, then classifies the `origin` URL by `github.com`, `gitlab`, or `bitbucket`, falling back to `none`. Successful remote reads are cached for five minutes. PR routes and the watcher use the resolved provider. **Kōbō ships no forge credentials**: users authenticate `gh`/`glab` themselves; Bitbucket uses the email and API token entered in Settings, passed to `bkt` and excluded from settings exports. Missing or unauthenticated CLIs disable PR/MR actions with an explanation.
|
|
182
208
|
|
|
183
209
|
### Notion (opt-in, user-provided credentials)
|
|
184
210
|
|
|
@@ -188,7 +214,7 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
188
214
|
2. `NOTION_TOKEN` env var
|
|
189
215
|
3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config: the recommended path, the same token shared with Claude Code)
|
|
190
216
|
|
|
191
|
-
The
|
|
217
|
+
The selected Claude config entry defaults to the enabled key named exactly `notion`; `global.notionMcpKey` selects another exact key. Its command/args are reused unless `NOTION_MCP_COMMAND` / `NOTION_MCP_ARGS` override them; otherwise the fallback is `npx -y @notionhq/notion-mcp-server`. Existing `OPENAPI_MCP_HEADERS` are preserved; when absent, the resolved token is used to construct them.
|
|
192
218
|
|
|
193
219
|
When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent; only the explicit Notion import endpoints should fail with a clear error. Do not throw at server startup.
|
|
194
220
|
|
|
@@ -198,7 +224,7 @@ See the "Notion integration" section of the README for the end-user setup guide.
|
|
|
198
224
|
|
|
199
225
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
200
226
|
|
|
201
|
-
**Claude Code** (`claude-code/`):
|
|
227
|
+
**Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
|
|
202
228
|
|
|
203
229
|
**OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
204
230
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -220,17 +246,60 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
220
246
|
- **Permission mode vs collaboration mode are independent.** Sandbox + approvalPolicy control *what the agent may do at OS level* (read-only / workspace-write, never / on-request / unless-trusted). `collaborationMode` is a separate session-level flag that gates internal Codex behaviour (notably interactive Q&A). Kōbō hides both behind a single "permission mode" selector and maps them together.
|
|
221
247
|
- **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call, the same dual-emission Claude does. See `event-mapper.ts` `handleItemStarted` / `handleItemCompleted` for the `collabAgentToolCall` branch.
|
|
222
248
|
- **`fileChange` items carry a unified-diff blob.** The protocol shape is `{ path, kind: PatchChangeKind, diff: string }` per change; `kind` is a discriminated union, not a string. The mapper flattens the first change into a Claude-style Edit input (`{ file_path, diff, change_kind, move_path? }`) so the existing `ToolCallItem` renderer picks it up. The client parses the unified diff into `DiffLine[]` via `parseUnifiedDiff` in `inline-diff.ts`.
|
|
223
|
-
- **Streaming bursts trip auto-scroll.**
|
|
249
|
+
- **Streaming bursts trip auto-scroll.** Both engines can emit token deltas; Claude enables SDK partial messages as well. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. Scroll requests are coalesced through `requestAnimationFrame`; preserve burst handling when changing the feed.
|
|
224
250
|
- **`MCP tools` need `default_tools_approval_mode: 'auto'` in `config.mcp_servers`.** Without it Codex flags every MCP tool call as needing user approval ("user cancelled MCP tool call"). Kōbō trusts every tool it spawns, so the options-builder pre-approves the namespace.
|
|
225
251
|
|
|
226
252
|
## Workspace operations
|
|
227
253
|
|
|
254
|
+
### Attachments during creation
|
|
255
|
+
|
|
256
|
+
`CreationAttachments.vue` keeps `File[]` in the create form, with image-only
|
|
257
|
+
object-URL previews revoked on removal/unmount. The workspace store sends
|
|
258
|
+
existing JSON requests unchanged when there are no files; attachments use
|
|
259
|
+
multipart fields `workspace` (one JSON string) and repeated `attachments`.
|
|
260
|
+
The legacy `images` field is accepted too; both fields share validation limits.
|
|
261
|
+
`attachment-service.ts` validates before workspace/git side effects,
|
|
262
|
+
then saves after the worktree exists and before setup/agent launch. Images use
|
|
263
|
+
`image-service.ts`; documents use generated filenames in `.ai/attachments/`,
|
|
264
|
+
exclusive writes and the existing directory-containment helper. File ownership
|
|
265
|
+
is recorded immediately after exclusive open so partial writes are cleaned on
|
|
266
|
+
failure. Creation excludes `.ai/attachments/` from Git; document uploads also
|
|
267
|
+
ensure that exclusion for existing workspaces. Never use an
|
|
268
|
+
uploaded filename as a storage path. The initial prompt stores image references
|
|
269
|
+
and document names/paths for normal starts and setup retries. Documents are
|
|
270
|
+
copied byte-for-byte, with no PDF extraction or OCR.
|
|
271
|
+
Limits and supported formats live in `src/shared/attachments.ts`:
|
|
272
|
+
10 files, 50 MiB each, 50 MiB total; the creation route limits the body to 51 MiB.
|
|
273
|
+
Document validation uses supported filename extensions because browser MIME
|
|
274
|
+
labels can be empty or generic. A failed creation cleans its own uploaded files
|
|
275
|
+
from reused worktrees, preserving existing files; setup failures retain the
|
|
276
|
+
workspace, files and initial prompt. Comparison requests copy the same browser
|
|
277
|
+
files independently. Attachments are not persisted in form presets.
|
|
278
|
+
See [creation attachments](CONFIGURATION.md#attachments-in-the-creation-description).
|
|
279
|
+
|
|
280
|
+
### Chat attachments
|
|
281
|
+
|
|
282
|
+
`POST /api/workspaces/:id/attachments` in `routes/images.ts` accepts one multipart
|
|
283
|
+
`attachment`, validates it with the shared `attachments.ts` policy and returns
|
|
284
|
+
`uid`, `kind`, `path`, `originalName`, and `reference`. It uses the same storage as
|
|
285
|
+
creation and a lifecycle guard against concurrent purge/delete. Document deletion
|
|
286
|
+
accepts only the generated filename at `DELETE /:id/attachments/:filename`; images
|
|
287
|
+
retain their existing retrieval/deletion endpoints. The legacy image upload also
|
|
288
|
+
uses the 50 MiB limit.
|
|
289
|
+
|
|
290
|
+
`use-chat-attachments.ts` owns pending upload state, draft-level limits and serial
|
|
291
|
+
uploads. `ChatInput.vue` transfers file ownership when sending/queuing, restores
|
|
292
|
+
badges on failed sends, and discards unsent files on workspace/session changes.
|
|
293
|
+
Late upload receipts are cleaned up using the captured original workspace id.
|
|
294
|
+
Never delete handed-off files when clearing a draft or changing conversations.
|
|
295
|
+
See [chat attachments](CONFIGURATION.md#attachments-in-workspace-chat).
|
|
296
|
+
|
|
228
297
|
### Change source branch
|
|
229
298
|
|
|
230
299
|
`change-source-branch-service.ts` re-targets a workspace onto a new source branch. The default path is a cherry-pick of the branch-proper commits (commits in the working branch but in **neither** the old nor the new base), inspired by the sekur `deploy-preprod-rebase.yml` workflow. The route is `POST /api/workspaces/:id/change-source-branch` and returns a discriminated status: `done | aligned | conflict | too-many | dirty`.
|
|
231
300
|
|
|
232
301
|
- **Built-in cherry-pick**: `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
|
|
233
|
-
- **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty;
|
|
302
|
+
- **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty; clearing the effective script hides the action; the global default is the bundled Kōbō script.
|
|
234
303
|
- **Custom-script env vars**: `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
|
|
235
304
|
|
|
236
305
|
### Lifecycle hooks
|
|
@@ -263,9 +332,9 @@ One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `
|
|
|
263
332
|
|
|
264
333
|
`src/server/services/worktree-purge-service.ts` removes a workspace's worktree from disk while preserving the chat history and PR metadata. Triggered manually via `POST /api/workspaces/:id/purge-worktree` from the workspace context menu, or automatically by the pr-watcher when a PR transitions to MERGED **and** `global.autoPurgeOnPrMerged` is enabled (Settings → Worktrees toggle, settings migration v36).
|
|
265
334
|
|
|
266
|
-
Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`)
|
|
335
|
+
Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`) trigger an attempt to reclaim ownership through a temporary Docker container and retry removal. If recovery fails, the UI offers manual cleanup guidance and prevention tips (Docker `USER`, default ACLs). Agent and dev-server shutdown must be confirmed before removal; shutdown failures leave the worktree in place. See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
|
|
267
336
|
|
|
268
|
-
**Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`,
|
|
337
|
+
**Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, validates the exact worktree root, repository and working branch via `isMatchingWorkspaceWorktree`, and on a match calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
|
|
269
338
|
|
|
270
339
|
**Restore**: `POST /api/workspaces/:id/restore-worktree` calls `worktree-restore-service.ts` to recreate the exact checkout and only then clear purge/archive metadata via `restoreWorktreeFromDisk`. Source order: surviving local branch → optional `headCommitSha` in the existing restore JSON → exact branch fetched from `origin`. No SQL migration is needed for the optional JSON key; older records remain supported. No reset, overwrite, agent/dev-server start or setup script. `workspace-lifecycle-guard.ts` excludes overlapping restore/purge/delete operations, while the existing common-Git-directory lock serializes Git mutations. The watcher shares `isMatchingWorkspaceWorktree` validation and invalidates in-flight PR checks on restoration; delayed auto-purges carry their expected archive timestamp to avoid purging a workspace restored in the meantime. The context menu and purged banner expose the action; HTTP and the existing `workspace:worktree-restored` event reconcile client lists. Discarded uncommitted/ignored files and dependencies are not recoverable.
|
|
271
340
|
|
|
@@ -295,7 +364,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
|
|
|
295
364
|
|
|
296
365
|
**Route layer** is thin: always wrap the handler body in `try / catch` and return `c.json({ error: message }, status)`. Match the existing shape in `src/server/routes/workspaces.ts`.
|
|
297
366
|
|
|
298
|
-
**
|
|
367
|
+
**Best-effort failures** may be logged and swallowed for non-critical side effects such as lifecycle-hook reporting. Process shutdown is a precondition for destructive operations: use `stopAgentAndWait` with `assertAgentStopped`, propagate dev-server shutdown failures, and retain the worktree when shutdown is unconfirmed. See [Audit remediation contracts](#audit-remediation-contracts-september-2026).
|
|
299
368
|
|
|
300
369
|
**Route ordering matters** in Hono. Static paths (`GET /archived`) MUST be declared **before** dynamic segments (`GET /:id`) or the dynamic segment captures them. There's a regression test locking this invariant in `src/__tests__/routes-workspaces.test.ts`.
|
|
301
370
|
|
|
@@ -305,7 +374,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
|
|
|
305
374
|
|
|
306
375
|
## Internationalization (i18n)
|
|
307
376
|
|
|
308
|
-
The frontend uses `vue-i18n`
|
|
377
|
+
The frontend uses `vue-i18n` with 5 supported locales: English (`en`), French (`fr`), German (`de`), Spanish (`es`), Italian (`it`). Translation files live in `src/client/src/i18n/`; the dependency version is declared in `src/client/package.json`.
|
|
309
378
|
|
|
310
379
|
**Mandatory rules for all frontend code:**
|
|
311
380
|
|
|
@@ -318,9 +387,9 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
|
|
|
318
387
|
|
|
319
388
|
## Testing discipline
|
|
320
389
|
|
|
321
|
-
- **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See
|
|
390
|
+
- **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See `src/__tests__/workspace-service.test.ts` for the setup pattern: `resetDb()` creates a fresh database in a temporary directory, initializes its schema, and closes/removes it after each test.
|
|
322
391
|
- **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete; missing exports cause obscure failures.
|
|
323
|
-
- **Frontend tests** cover Pinia stores
|
|
392
|
+
- **Frontend tests** cover Pinia stores, pure utilities, composables, and selected Vue components (for example `ActivityFeed`, `TurnCard`, and `WorkspaceCard`). Run with `cd src/client && npm test`; use Vue Test Utils and the existing `happy-dom` setup for component regressions. Run `cd src/client && npm run type-check` for Vue/TypeScript checking, and use manual smoke tests for browser behavior that the DOM test environment cannot exercise.
|
|
324
393
|
- **`beforeEach(() => vi.clearAllMocks())`** is the convention for all route test files.
|
|
325
394
|
|
|
326
395
|
## Git workflow
|
|
@@ -385,7 +454,7 @@ deviate from the documented system without explicit user approval.
|
|
|
385
454
|
- Don't add confirmation dialogs for reversible actions (archive, unarchive). Only destructive actions (delete) get a dialog.
|
|
386
455
|
- Don't introduce ORMs, query builders, or schema validation libraries; the project is small enough for raw prepared statements and hand-written mappers.
|
|
387
456
|
- Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
|
|
388
|
-
- Don't
|
|
457
|
+
- Don't swallow failed or timed-out shutdowns before deleting, purging, or replacing a worktree. Only non-critical best-effort side effects may fail without blocking the primary operation.
|
|
389
458
|
- Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
|
|
390
459
|
- Don't hardcode hex colors, spacing literals, or font names in components. Use the CSS variables from `src/client/src/css/design-tokens.scss` and the patterns in `DESIGN.md`. See [Design System](#design-system).
|
|
391
460
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.11.41
|
|
8
|
+
|
|
9
|
+
- feat(attachments): support documents in workspace creation and chat
|
|
10
|
+
|
|
11
|
+
## 1.11.40
|
|
12
|
+
|
|
13
|
+
- feat(workspaces): add external MCP access and harden workspace flows
|
|
14
|
+
|
|
7
15
|
## 1.11.39
|
|
8
16
|
|
|
9
17
|
- fix(deps): require Claude SDK with Fable 5.1 support
|
package/README.md
CHANGED
|
@@ -38,14 +38,16 @@ Default port is `3000`; if it's taken, `SERVER_PORT` (checked first) or `PORT` p
|
|
|
38
38
|
SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Kōbō's production build is an installable PWA — use your browser's **Install app** action. Want to run from source or contribute instead? See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Prefer Docker? Jump to [Docker deployment](#docker
|
|
41
|
+
Kōbō's production build is an installable PWA — use your browser's **Install app** action. Want to run from source or contribute instead? See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Prefer Docker? Jump to [Docker deployment](#docker).
|
|
42
42
|
|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
## Everything else Kōbō does
|
|
46
46
|
|
|
47
|
+
- **Attachments from the first prompt**: paste, drop or attach screenshots, Markdown, text files or PDFs to the creation description or workspace chat. Kōbō copies them into the worktree for the agent. See [creation attachments](./CONFIGURATION.md#attachments-in-the-creation-description) and [chat attachments](./CONFIGURATION.md#attachments-in-workspace-chat).
|
|
48
|
+
|
|
47
49
|
- **Command palette & search**: `/` autocompletes skills and commands, `@` fuzzy-completes worktree file paths, `Ctrl+F` searches readable messages, `Ctrl+K` opens a command palette. The global search page deep-links to the same spot.
|
|
48
|
-
- **
|
|
50
|
+
- **MCP toolset (`kobo-tasks`)**: task/acceptance-criteria CRUD, dev server control, a unified `get_ticket` (Notion or Sentry), cross-workspace conversation search, per-session usage, and a `.ai/thoughts` decision log — see the [MCP guide](./src/mcp-server/README.md#tools). External MCP clients can also discover workspaces, read conversations, send messages, and answer questions through HTTP or global stdio.
|
|
49
51
|
|
|
50
52
|

|
|
51
53
|
- **Multi-forge Git panel**: GitHub (`gh`), GitLab (`glab`), or Bitbucket Community (`bkt`), auto-detected from the remote — `Open PR`, `Merge ready PR`, `Change PR base`, `Change source branch`, all from the UI.
|
|
@@ -56,7 +58,7 @@ Kōbō's production build is an installable PWA — use your browser's **Install
|
|
|
56
58
|

|
|
57
59
|
- **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets live in the footer; sessions auto-resume after a reset.
|
|
58
60
|
- **Disk-space purge**: reclaim a merged workspace's `node_modules`/`vendor` weight without losing its chat history — see [`CONFIGURATION.md`](./CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
|
|
59
|
-
- **Lifecycle scripts**: shell scripts run
|
|
61
|
+
- **Lifecycle scripts**: shell scripts run on setup, cleanup, archive, session end, PR merge, or auto-loop stop, with output streamed into the chat.
|
|
60
62
|
- **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
|
|
61
63
|
- **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
|
|
62
64
|
|
|
@@ -105,7 +107,7 @@ src/
|
|
|
105
107
|
├── client/ # Vue 3 + Quasar PWA
|
|
106
108
|
├── mcp-server/ # kobo-tasks MCP server, spawned per workspace
|
|
107
109
|
├── shared/ # types shared backend ↔ frontend
|
|
108
|
-
└── __tests__/ # Vitest suite
|
|
110
|
+
└── __tests__/ # backend Vitest suite; client tests live in client/src/__tests__/
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
[`AGENTS.md`](./AGENTS.md) covers the data model, WebSocket protocol, engine contracts, MCP tool surface, migration discipline, i18n rules, and contribution guidelines.
|
|
@@ -4,7 +4,7 @@ import { nanoid } from 'nanoid';
|
|
|
4
4
|
import * as cronService from '../server/services/cron-service.js';
|
|
5
5
|
import * as settingsService from '../server/services/settings-service.js';
|
|
6
6
|
import { slugifyProjectName } from '../server/utils/project-slug.js';
|
|
7
|
-
import { resolveExistingPathInside } from '../server/utils/safe-path.js';
|
|
7
|
+
import { ensureDirectoryInside, resolveExistingPathInside } from '../server/utils/safe-path.js';
|
|
8
8
|
import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
|
|
9
9
|
import { MASKED_SECRET, SECRET_GLOBAL_KEYS } from '../shared/consts.js';
|
|
10
10
|
/** Allowed task status values. */
|
|
@@ -404,7 +404,7 @@ export function readDocumentHandler(worktreePath, relPath) {
|
|
|
404
404
|
}
|
|
405
405
|
/**
|
|
406
406
|
* Append a thought / decision / note to
|
|
407
|
-
* `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>.md`. Creates the directory if missing.
|
|
407
|
+
* `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>-<timestamp>-<id>.md`. Creates the directory if missing.
|
|
408
408
|
* Returns the path (worktree-relative) of the file actually written — useful
|
|
409
409
|
* for the agent to reference it in chat.
|
|
410
410
|
*
|
|
@@ -419,8 +419,7 @@ export function logThoughtHandler(worktreePath, data) {
|
|
|
419
419
|
const content = data.content?.trim();
|
|
420
420
|
if (!content)
|
|
421
421
|
throw new Error('content is required');
|
|
422
|
-
const thoughtsDir = path.join(
|
|
423
|
-
fs.mkdirSync(thoughtsDir, { recursive: true });
|
|
422
|
+
const thoughtsDir = ensureDirectoryInside(worktreePath, path.join('.ai', 'thoughts', 'logs'));
|
|
424
423
|
const date = new Date().toISOString().slice(0, 10);
|
|
425
424
|
const slug = title
|
|
426
425
|
.toLowerCase()
|
|
@@ -430,12 +429,20 @@ export function logThoughtHandler(worktreePath, data) {
|
|
|
430
429
|
.replace(/^-+|-+$/g, '')
|
|
431
430
|
.slice(0, 60) || 'note';
|
|
432
431
|
const tagSuffix = data.tag ? `-${data.tag.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}` : '';
|
|
433
|
-
const filename = `${date}-${slug}${tagSuffix}.md`;
|
|
434
|
-
const abs = path.join(thoughtsDir, filename);
|
|
435
|
-
const relPath = `.ai/thoughts/logs/${filename}`;
|
|
436
432
|
const header = `# ${title}\n\n_${new Date().toISOString()}_${data.tag ? ` · tag: \`${data.tag}\`` : ''}\n\n`;
|
|
437
|
-
|
|
438
|
-
|
|
433
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
434
|
+
const filename = `${date}-${slug}${tagSuffix}-${Date.now()}-${nanoid(10)}.md`;
|
|
435
|
+
const abs = path.join(thoughtsDir, filename);
|
|
436
|
+
try {
|
|
437
|
+
fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), { encoding: 'utf-8', flag: 'wx' });
|
|
438
|
+
return { path: `.ai/thoughts/logs/${filename}` };
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
if (err.code !== 'EEXIST')
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
throw new Error('Could not allocate a unique thought filename');
|
|
439
446
|
}
|
|
440
447
|
/**
|
|
441
448
|
* Aggregate `usage` events from `ws_events` to report how many tokens and
|
|
@@ -3,7 +3,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { getDb } from '../server/db/index.js';
|
|
6
|
+
import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
|
|
6
7
|
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
8
|
+
import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
|
|
7
9
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
8
10
|
const dbPath = process.env.KOBO_DB_PATH;
|
|
9
11
|
const settingsPath = process.env.KOBO_SETTINGS_PATH;
|
|
@@ -94,9 +96,12 @@ async function notifyAgentDescriptionUpdated() {
|
|
|
94
96
|
/** Generic HTTP request to the Kobo backend, returning parsed JSON or null. */
|
|
95
97
|
async function backendRequest(method, pathname, body) {
|
|
96
98
|
const url = `${backendUrl}${pathname}`;
|
|
97
|
-
const
|
|
99
|
+
const headers = process.env.KOBO_NETWORK_TOKEN
|
|
100
|
+
? { 'X-Kobo-Token': process.env.KOBO_NETWORK_TOKEN }
|
|
101
|
+
: {};
|
|
102
|
+
const init = { method, headers };
|
|
98
103
|
if (body !== undefined) {
|
|
99
|
-
|
|
104
|
+
headers['Content-Type'] = 'application/json';
|
|
100
105
|
init.body = JSON.stringify(body);
|
|
101
106
|
}
|
|
102
107
|
const res = await fetch(url, init);
|
|
@@ -475,6 +480,7 @@ const WORKSPACE_SCOPED_TOOLS = [
|
|
|
475
480
|
},
|
|
476
481
|
];
|
|
477
482
|
const GLOBAL_TOOLS = [
|
|
483
|
+
...WORKSPACE_DIALOGUE_TOOLS,
|
|
478
484
|
{
|
|
479
485
|
name: 'list_workspaces',
|
|
480
486
|
description: 'List Kōbō workspaces with their id, title, status, and creation date. Works even when the Kōbō backend server is not running (reads the database directly). Use this to discover existing workspaces before creating one or targeting one with archive_workspace/stop_workspace.',
|
|
@@ -594,6 +600,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
594
600
|
return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
|
|
595
601
|
}
|
|
596
602
|
try {
|
|
603
|
+
if (WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
|
|
604
|
+
if (workspaceId && a.workspace_id === workspaceId && name === 'send_workspace_message')
|
|
605
|
+
return fail('Use the normal conversation to communicate in your own workspace.');
|
|
606
|
+
return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
|
|
607
|
+
}
|
|
597
608
|
if (name === 'list_tasks') {
|
|
598
609
|
return ok(listTasksHandler(db, workspaceId));
|
|
599
610
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
|
+
import { validateDialogueArguments } from '../shared/workspace-dialogue-tools.js';
|
|
4
|
+
import { normalizeClientName } from '../shared/workspace-message-types.js';
|
|
5
|
+
/** Stateless HTTP bridge: external stdio clients never own agent runtime state. */
|
|
6
|
+
export async function callWorkspaceDialogueTool(backendUrl, name, args, token, clientName) {
|
|
7
|
+
const arguments_ = validateDialogueArguments(name, args);
|
|
8
|
+
const client = new Client({ name: 'kobo-stdio-bridge', version: '1.0.0' });
|
|
9
|
+
const signal = AbortSignal.timeout(30_000);
|
|
10
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${backendUrl.replace(/\/$/, '')}/api/mcp`), {
|
|
11
|
+
requestInit: {
|
|
12
|
+
signal,
|
|
13
|
+
headers: {
|
|
14
|
+
...(token ? { 'X-Kobo-Token': token } : {}),
|
|
15
|
+
'X-Kobo-Client-Name': encodeURIComponent(normalizeClientName(clientName)),
|
|
16
|
+
'X-Kobo-Client-Name-Encoding': 'uri',
|
|
17
|
+
'X-Kobo-Client-Transport': 'stdio',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
await client.connect(transport, { signal });
|
|
23
|
+
return await client.callTool({ name, arguments: arguments_ }, undefined, { signal });
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await client.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** v43 schema shared by fresh installs and the append-only migration. */
|
|
2
|
+
export function initMcpMessageSchema(db) {
|
|
3
|
+
db.exec(`CREATE TABLE IF NOT EXISTS mcp_message_requests (
|
|
4
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
5
|
+
idempotency_key TEXT NOT NULL,
|
|
6
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
7
|
+
fingerprint TEXT NOT NULL,
|
|
8
|
+
state TEXT NOT NULL CHECK (state IN ('reserved','dispatching','accepted','rejected','unknown')),
|
|
9
|
+
result_json TEXT,
|
|
10
|
+
event_id TEXT,
|
|
11
|
+
session_id TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (workspace_id, idempotency_key)
|
|
15
|
+
)`);
|
|
16
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { initMcpMessageSchema } from './mcp-message-schema.js';
|
|
1
2
|
import { initSchema } from './schema.js';
|
|
3
|
+
import { initSearchSchema } from './search-schema.js';
|
|
2
4
|
/** Ordered registry of all schema migrations. Append new entries at the end. */
|
|
3
5
|
export const migrations = [
|
|
4
6
|
{
|
|
@@ -740,6 +742,8 @@ export const migrations = [
|
|
|
740
742
|
CREATE INDEX IF NOT EXISTS idx_workspace_activity_created ON workspace_activity(created_at);`);
|
|
741
743
|
},
|
|
742
744
|
},
|
|
745
|
+
{ version: 42, name: 'index-logical-search-messages', migrate: initSearchSchema },
|
|
746
|
+
{ version: 43, name: 'durable-mcp-message-requests', migrate: initMcpMessageSchema },
|
|
743
747
|
];
|
|
744
748
|
/** Current schema version — always equals the highest migration version. */
|
|
745
749
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { initMcpMessageSchema } from './mcp-message-schema.js';
|
|
2
|
+
import { initSearchSchema } from './search-schema.js';
|
|
1
3
|
/** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
|
|
2
4
|
export function initSchema(db) {
|
|
3
5
|
db.exec(`
|
|
@@ -221,4 +223,6 @@ export function initSchema(db) {
|
|
|
221
223
|
CREATE INDEX IF NOT EXISTS idx_session_event_metrics_session
|
|
222
224
|
ON session_event_metrics(session_id);
|
|
223
225
|
`);
|
|
226
|
+
initSearchSchema(db);
|
|
227
|
+
initMcpMessageSchema(db);
|
|
224
228
|
}
|