@loicngr/kobo 1.11.40 → 1.11.42
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 +110 -32
- package/CHANGELOG.md +8 -0
- package/README.md +6 -4
- package/dist/server/routes/images.js +65 -5
- package/dist/server/routes/workspaces.js +29 -10
- package/dist/server/services/activity-service.js +3 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
- package/dist/server/services/attachment-service.js +122 -0
- package/dist/server/services/image-service.js +16 -7
- package/dist/server/services/settings-service.js +13 -0
- package/dist/shared/attachments.js +58 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-MqsC-95i.js → ActivityFeed-CyCGH0Zr.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-BxURWYxa.js → ChangelogPage-B6xjI-I4.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-DjiMDAUi.js → ComparisonPanel-BmRzCu1c.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-DHe0x63K.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-TY9g3hZb.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-mA5RZf1t.js → DashboardPage-CRTkx9P-.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-Ci7ucmy3.js → DiffViewer-BJfLiYGs.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-RZTvcJkT.js → DocumentsPanel-B9jjzoqY.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-CTOc3yA0.js → DrawerToggleButton-CBZpPa30.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-gqySqt7G.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-D3__hKJ5.js → HealthPage-9VEvoR_4.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BM14JTBB.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-DxMVVA6f.css → MainLayout-Dhj28mOe.css} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-td0RAVPx.js → QBtnDropdown-Cnkq_5eu.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-BkyFPV7a.js → QDrawer-C3b-lbmv.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-WspK3KC7.js → QSelect-DJL6yy1E.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-7pw7iuV0.js → QTooltip-WIoDeLoZ.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-CLNM13oo.js → SchedulePanel-tVggDx-C.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-CmRgIvu4.js → SearchPage-l4m1qxKs.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-W6N01tGz.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-msHMBS7q.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-CCHGEsKf.js → SplitWorkspacePage-AXMYd-9b.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-ijmsYk-2.js → TerminalPanel-CENXl8po.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-DuQFjlQF.js → TouchPan-DaEpS0Vf.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-C5RF0lOK.js → TourReplayButton-U3mwT0fV.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BR_XElog.js → WorkspaceAttentionLabels-CpTA6VGb.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-DE6-wlf7.js +4 -0
- package/src/client/dist/pwa/assets/{browser-DUqzdkbg.js → browser-B4pgwnXH.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-ImiSpzfI.js → build-path-tree-6ENXqh7O.js} +1 -1
- package/src/client/dist/pwa/assets/{de-CaHID9mW.js → de-D9gPXN_a.js} +2 -2
- package/src/client/dist/pwa/assets/{documents-2C1hn1pw.js → documents-D88lOIxR.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-Bm1vVaBr.js → editor.api-qQNqM6Y6.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-_D8DFYXo.js → efforts-CMQi0Rp2.js} +1 -1
- package/src/client/dist/pwa/assets/{engineFeatures-CZ9qfBYq.js → engineFeatures-BPcXoXFd.js} +1 -1
- package/src/client/dist/pwa/assets/{es-ZhzlCmer.js → es-B4Q1ncyP.js} +2 -2
- package/src/client/dist/pwa/assets/expand-template-DuMdsD-k.js +1 -0
- package/src/client/dist/pwa/assets/{format-pPImdFUy.js → format-BVgznNN8.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-dTX38ZsV.js → formatters-DV1VAk5B.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-BfXw7L7J.js → fr-Qv-edVJn.js} +2 -2
- package/src/client/dist/pwa/assets/{freemarker2-DGluDnAm.js → freemarker2-Nt_Z9A9p.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BgviTnpD.js → handlebars-sJjXCA98.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CV-MrunL.js → html-B2wF0NKO.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-eiyc92m_.js +1 -0
- package/src/client/dist/pwa/assets/{index-9tiw3NjK.js → index-DtsrXdzx.js} +3 -3
- package/src/client/dist/pwa/assets/{it-CHAurcTF.js → it-D-qzfo9T.js} +2 -2
- package/src/client/dist/pwa/assets/{javascript-C2VFcHf2.js → javascript-BPbCoYVZ.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dg9ZhbP5.js → liquid-DBwLDEnw.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-ImgrPYNl.js → mdx-D_Sphj-P.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-waWZXS1T.js → monaco-contributions-BBjimLYs.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DNP7HNKq.js +2 -0
- package/src/client/dist/pwa/assets/{network-auth-h6O0qCMA.js → network-auth-BLGlHVxR.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-Dyg-FGI6.js → notify-theme-tvaTlaFw.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-Dfpg92li.js → pending-diff-open-CxtcrwVN.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-DDWmNBW1.js → permissionModes-CESk18Vk.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-XUfY_zbw.js → project-color-D75XmBNA.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DVLs-DBA.js → python-BdBTal4f.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-D34BqMwB.js → razor-gJNlRRte.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DwUkPy-O.js → render-chat-markdown-K3tUW_1p.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-C7-UpNXx.js → typescript-DAGixRVW.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-DfE-kwrX.js → use-tours-CS5oBFk6.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-DwwdOEAq.js → workspace-templates-BoGUaV1C.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-Cx1oQhEr.js → xml-BC9pt2ZZ.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BQOiJsdx.js → yaml-CQpOdW-l.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 +51 -7
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +0 -2
- package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +0 -2
- package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +0 -9
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-CqcMweNL.css +0 -1
- package/src/client/dist/pwa/assets/expand-template-CbDNnTPP.js +0 -1
- package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +0 -1
- package/src/client/dist/pwa/assets/monaco.contribution-BB2rtCHM.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,7 +170,8 @@ 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
|
|
@@ -180,7 +194,12 @@ Clients subscribe to individual workspace ids. The server sends `WsEvent` object
|
|
|
180
194
|
{ id, workspaceId, type, payload, sessionId?, createdAt }
|
|
181
195
|
```
|
|
182
196
|
|
|
183
|
-
|
|
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.
|
|
198
|
+
|
|
199
|
+
Error events may carry a stable `code`. Claude's post-result drain timeout uses
|
|
200
|
+
`result_drain_timeout`: keep it in the chat but exclude it from `AgentErrorBanner`.
|
|
201
|
+
The banner selector also recognizes the exact legacy message for persisted
|
|
202
|
+
history. Do not suppress other watchdog errors or change session-end/retry behavior.
|
|
184
203
|
|
|
185
204
|
Two emit flavors in `websocket-service.ts`:
|
|
186
205
|
- `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
|
|
@@ -190,7 +209,7 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
190
209
|
|
|
191
210
|
### Forge providers
|
|
192
211
|
|
|
193
|
-
`src/server/services/forge/` implements
|
|
212
|
+
`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.
|
|
194
213
|
|
|
195
214
|
### Notion (opt-in, user-provided credentials)
|
|
196
215
|
|
|
@@ -200,7 +219,7 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
200
219
|
2. `NOTION_TOKEN` env var
|
|
201
220
|
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)
|
|
202
221
|
|
|
203
|
-
The
|
|
222
|
+
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.
|
|
204
223
|
|
|
205
224
|
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.
|
|
206
225
|
|
|
@@ -210,7 +229,7 @@ See the "Notion integration" section of the README for the end-user setup guide.
|
|
|
210
229
|
|
|
211
230
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
212
231
|
|
|
213
|
-
**Claude Code** (`claude-code/`):
|
|
232
|
+
**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`.
|
|
214
233
|
|
|
215
234
|
**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:
|
|
216
235
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -232,17 +251,69 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
232
251
|
- **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.
|
|
233
252
|
- **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.
|
|
234
253
|
- **`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`.
|
|
235
|
-
- **Streaming bursts trip auto-scroll.**
|
|
254
|
+
- **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.
|
|
236
255
|
- **`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.
|
|
237
256
|
|
|
238
257
|
## Workspace operations
|
|
239
258
|
|
|
259
|
+
### Attachments during creation
|
|
260
|
+
|
|
261
|
+
`CreationAttachments.vue` keeps `File[]` in the create form, with image-only
|
|
262
|
+
object-URL previews revoked on removal/unmount. The workspace store sends
|
|
263
|
+
existing JSON requests unchanged when there are no files; attachments use
|
|
264
|
+
multipart fields `workspace` (one JSON string) and repeated `attachments`.
|
|
265
|
+
The legacy `images` field is accepted too; both fields share validation limits.
|
|
266
|
+
`attachment-service.ts` validates before workspace/git side effects,
|
|
267
|
+
then saves after the worktree exists and before setup/agent launch. Images use
|
|
268
|
+
`image-service.ts`; documents use generated filenames in `.ai/attachments/`,
|
|
269
|
+
exclusive writes and the existing directory-containment helper. File ownership
|
|
270
|
+
is recorded immediately after exclusive open so partial writes are cleaned on
|
|
271
|
+
failure. Creation excludes `.ai/attachments/` from Git; document uploads also
|
|
272
|
+
ensure that exclusion for existing workspaces. Never use an
|
|
273
|
+
uploaded filename as a storage path. The initial prompt stores image references
|
|
274
|
+
and document names/paths for normal starts and setup retries. Documents are
|
|
275
|
+
copied byte-for-byte, with no PDF extraction or OCR.
|
|
276
|
+
Limits and supported formats live in `src/shared/attachments.ts`:
|
|
277
|
+
10 files, 50 MiB each, 50 MiB total; the creation route limits the body to 51 MiB.
|
|
278
|
+
Document validation uses supported filename extensions because browser MIME
|
|
279
|
+
labels can be empty or generic. A failed creation cleans its own uploaded files
|
|
280
|
+
from reused worktrees, preserving existing files; setup failures retain the
|
|
281
|
+
workspace, files and initial prompt. Comparison requests copy the same browser
|
|
282
|
+
files independently. Attachments are not persisted in form presets.
|
|
283
|
+
See [creation attachments](CONFIGURATION.md#attachments-in-the-creation-description).
|
|
284
|
+
|
|
285
|
+
### Chat attachments
|
|
286
|
+
|
|
287
|
+
`POST /api/workspaces/:id/attachments` in `routes/images.ts` accepts one multipart
|
|
288
|
+
`attachment`, validates it with the shared `attachments.ts` policy and returns
|
|
289
|
+
`uid`, `kind`, `path`, `originalName`, and `reference`. It uses the same storage as
|
|
290
|
+
creation and a lifecycle guard against concurrent purge/delete. Document deletion
|
|
291
|
+
accepts only the generated filename at `DELETE /:id/attachments/:filename`; images
|
|
292
|
+
retain their existing retrieval/deletion endpoints. The legacy image upload also
|
|
293
|
+
uses the 50 MiB limit.
|
|
294
|
+
|
|
295
|
+
`use-chat-attachments.ts` owns pending upload state, draft-level limits and serial
|
|
296
|
+
uploads. `ChatInput.vue` transfers file ownership when sending/queuing, restores
|
|
297
|
+
badges on failed sends, and discards unsent files on workspace/session changes.
|
|
298
|
+
Late upload receipts are cleaned up using the captured original workspace id.
|
|
299
|
+
Never delete handed-off files when clearing a draft or changing conversations.
|
|
300
|
+
See [chat attachments](CONFIGURATION.md#attachments-in-workspace-chat).
|
|
301
|
+
|
|
302
|
+
### Setup during PR import
|
|
303
|
+
|
|
304
|
+
Resolving a PR checkout enables `skipSetupScript` by default, but the create
|
|
305
|
+
form leaves this toggle editable for PR imports. `resolveCreateOverrides`
|
|
306
|
+
preserves that choice and the request explicitly sends false when setup is wanted.
|
|
307
|
+
After successful PR extraction, the server runs setup only for an explicit
|
|
308
|
+
`skipSetupScript: false`; an omitted field continues to skip it. Ordinary worktree
|
|
309
|
+
reuse outside PR import still forces setup off. Worktree ownership is unchanged.
|
|
310
|
+
|
|
240
311
|
### Change source branch
|
|
241
312
|
|
|
242
313
|
`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`.
|
|
243
314
|
|
|
244
315
|
- **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.
|
|
245
|
-
- **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;
|
|
316
|
+
- **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.
|
|
246
317
|
- **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).
|
|
247
318
|
|
|
248
319
|
### Lifecycle hooks
|
|
@@ -275,9 +346,9 @@ One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `
|
|
|
275
346
|
|
|
276
347
|
`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).
|
|
277
348
|
|
|
278
|
-
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`)
|
|
349
|
+
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).
|
|
279
350
|
|
|
280
|
-
**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`,
|
|
351
|
+
**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.
|
|
281
352
|
|
|
282
353
|
**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.
|
|
283
354
|
|
|
@@ -289,6 +360,13 @@ Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / m
|
|
|
289
360
|
|
|
290
361
|
`activity-service.ts` records significant metadata from `emit` and `emitEphemeral` in `workspace_activity` (migration v41, 30-day retention). It excludes streaming data and superseded session endings. `/api/activity` exposes a paginated monotonic cursor; `stores/activity.ts` keeps browser-local visit checkpoints and protects against late responses after visibility changes. `ActivityDigest.vue` links events to workspace/session or Git context. Never advance an unread checkpoint to the head of unloaded pages.
|
|
291
362
|
|
|
363
|
+
`global.activityDigestEnabled` (settings migration v58, default true) controls the
|
|
364
|
+
feature from Settings → Notifications. When false, `recordActivity` skips new
|
|
365
|
+
journal entries; `ActivityDigest.vue` hides its entry/dialog and removes its timer
|
|
366
|
+
and visibility/network listeners. Wait for settings to load before starting the
|
|
367
|
+
tracker. Disabling invalidates in-flight activity responses without acknowledging
|
|
368
|
+
unread events or deleting history; re-enabling resumes from the saved checkpoint.
|
|
369
|
+
|
|
292
370
|
### Workspace attention indicators
|
|
293
371
|
|
|
294
372
|
`src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function, easy to unit-test and free of IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
|
|
@@ -307,7 +385,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
|
|
|
307
385
|
|
|
308
386
|
**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`.
|
|
309
387
|
|
|
310
|
-
**
|
|
388
|
+
**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).
|
|
311
389
|
|
|
312
390
|
**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`.
|
|
313
391
|
|
|
@@ -317,7 +395,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
|
|
|
317
395
|
|
|
318
396
|
## Internationalization (i18n)
|
|
319
397
|
|
|
320
|
-
The frontend uses `vue-i18n`
|
|
398
|
+
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`.
|
|
321
399
|
|
|
322
400
|
**Mandatory rules for all frontend code:**
|
|
323
401
|
|
|
@@ -330,9 +408,9 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
|
|
|
330
408
|
|
|
331
409
|
## Testing discipline
|
|
332
410
|
|
|
333
|
-
- **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
|
|
411
|
+
- **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.
|
|
334
412
|
- **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.
|
|
335
|
-
- **Frontend tests** cover Pinia stores
|
|
413
|
+
- **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.
|
|
336
414
|
- **`beforeEach(() => vi.clearAllMocks())`** is the convention for all route test files.
|
|
337
415
|
|
|
338
416
|
## Git workflow
|
|
@@ -397,7 +475,7 @@ deviate from the documented system without explicit user approval.
|
|
|
397
475
|
- Don't add confirmation dialogs for reversible actions (archive, unarchive). Only destructive actions (delete) get a dialog.
|
|
398
476
|
- Don't introduce ORMs, query builders, or schema validation libraries; the project is small enough for raw prepared statements and hand-written mappers.
|
|
399
477
|
- Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
|
|
400
|
-
- Don't
|
|
478
|
+
- 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.
|
|
401
479
|
- 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).
|
|
402
480
|
- 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).
|
|
403
481
|
|
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.42
|
|
8
|
+
|
|
9
|
+
- feat(workspaces): refine settings and session controls
|
|
10
|
+
|
|
11
|
+
## 1.11.41
|
|
12
|
+
|
|
13
|
+
- feat(attachments): support documents in workspace creation and chat
|
|
14
|
+
|
|
7
15
|
## 1.11.40
|
|
8
16
|
|
|
9
17
|
- feat(workspaces): add external MCP access and harden workspace flows
|
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.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
5
|
+
import { DOCUMENT_EXTENSIONS, MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_REQUEST_BYTES, validateAttachments, } from '../../shared/attachments.js';
|
|
6
|
+
import { attachmentReference, saveAttachments } from '../services/attachment-service.js';
|
|
4
7
|
import * as imageService from '../services/image-service.js';
|
|
5
8
|
import * as workspaceService from '../services/workspace-service.js';
|
|
6
|
-
import { isPathInside } from '../utils/safe-path.js';
|
|
7
|
-
|
|
8
|
-
const MAX_FILE_SIZE =
|
|
9
|
+
import { ensureDirectoryInside, isPathInside } from '../utils/safe-path.js';
|
|
10
|
+
import { WorkspaceLifecycleBusyError, withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
11
|
+
const MAX_FILE_SIZE = MAX_ATTACHMENT_BYTES;
|
|
9
12
|
/** MIME types accepted for image uploads. */
|
|
10
13
|
const ALLOWED_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
11
14
|
/** File extension → MIME type, used to serve uploaded images back to the frontend. */
|
|
@@ -18,8 +21,65 @@ const EXT_TO_MIME = {
|
|
|
18
21
|
};
|
|
19
22
|
/** Hono sub-router for workspace image upload and deletion. */
|
|
20
23
|
const app = new Hono();
|
|
24
|
+
// New chat uploads share formats, limits and storage with workspace creation.
|
|
25
|
+
app.post('/:id/attachments', bodyLimit({ maxSize: MAX_ATTACHMENT_REQUEST_BYTES }), async (c) => {
|
|
26
|
+
try {
|
|
27
|
+
const { id } = c.req.param();
|
|
28
|
+
let form;
|
|
29
|
+
try {
|
|
30
|
+
form = await c.req.raw.formData();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return c.json({ error: 'Invalid multipart body' }, 400);
|
|
34
|
+
}
|
|
35
|
+
const files = form.getAll('attachment');
|
|
36
|
+
if (files.length !== 1 || !(files[0] instanceof File))
|
|
37
|
+
return c.json({ error: 'Expected one attachment file' }, 400);
|
|
38
|
+
const file = files[0];
|
|
39
|
+
const error = validateAttachments([file]);
|
|
40
|
+
if (error)
|
|
41
|
+
return c.json({ error: `Invalid attachment: ${error}`, code: error }, 400);
|
|
42
|
+
return await withWorkspaceLifecycleGuard(id, async () => {
|
|
43
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
44
|
+
if (!workspace)
|
|
45
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
46
|
+
if (workspace.worktreePurgedAt)
|
|
47
|
+
return c.json({ error: 'Restore the worktree before uploading files' }, 409);
|
|
48
|
+
const [saved] = await saveAttachments(workspace.worktreePath, [file]);
|
|
49
|
+
return c.json({ ...saved, path: saved.relativePath, reference: attachmentReference(saved) }, 201);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, error instanceof WorkspaceLifecycleBusyError ? 409 : 500);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
app.delete('/:id/attachments/:filename', async (c) => {
|
|
57
|
+
try {
|
|
58
|
+
const { id, filename } = c.req.param();
|
|
59
|
+
// Accept only the generated document layout, never a caller-supplied path.
|
|
60
|
+
if (!/^[A-Za-z0-9_-]{10}\.[a-z]+$/.test(filename) ||
|
|
61
|
+
!DOCUMENT_EXTENSIONS.includes(path.extname(filename))) {
|
|
62
|
+
return c.json({ error: 'Invalid attachment filename' }, 400);
|
|
63
|
+
}
|
|
64
|
+
return await withWorkspaceLifecycleGuard(id, async () => {
|
|
65
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
66
|
+
if (!workspace)
|
|
67
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
68
|
+
if (workspace.worktreePurgedAt)
|
|
69
|
+
return c.json({ error: 'Worktree is purged' }, 409);
|
|
70
|
+
const directory = ensureDirectoryInside(workspace.worktreePath, '.ai/attachments');
|
|
71
|
+
// unlink removes a replaced leaf symlink itself; it never follows it.
|
|
72
|
+
fs.unlinkSync(path.join(directory, filename));
|
|
73
|
+
return c.body(null, 204);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const missing = error.code === 'ENOENT';
|
|
78
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, error instanceof WorkspaceLifecycleBusyError ? 409 : missing ? 404 : 500);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
21
81
|
// POST /:id/images — upload an image
|
|
22
|
-
app.post('/:id/images', async (c) => {
|
|
82
|
+
app.post('/:id/images', bodyLimit({ maxSize: MAX_ATTACHMENT_REQUEST_BYTES }), async (c) => {
|
|
23
83
|
try {
|
|
24
84
|
const { id } = c.req.param();
|
|
25
85
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -37,7 +97,7 @@ app.post('/:id/images', async (c) => {
|
|
|
37
97
|
const arrayBuffer = await file.arrayBuffer();
|
|
38
98
|
const buffer = Buffer.from(arrayBuffer);
|
|
39
99
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
40
|
-
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Max:
|
|
100
|
+
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Max: 50 MB` }, 400);
|
|
41
101
|
}
|
|
42
102
|
const worktreePath = workspace.worktreePath;
|
|
43
103
|
const result = await imageService.saveImage(worktreePath, buffer, file.name);
|