@loicngr/kobo 2.1.15 → 2.1.18

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.
Files changed (128) hide show
  1. package/AGENTS.md +61 -0
  2. package/CHANGELOG.md +12 -0
  3. package/CONFIGURATION.md +74 -17
  4. package/README.md +7 -3
  5. package/dist/mcp-server/kobo-tasks-server.js +19 -104
  6. package/dist/mcp-server/workspace-dialogue-client.js +15 -3
  7. package/dist/server/db/migrations.js +2 -0
  8. package/dist/server/db/schema.js +2 -0
  9. package/dist/server/db/workspace-message-batch-schema.js +9 -0
  10. package/dist/server/index.js +8 -0
  11. package/dist/server/middleware/workspace-creation-maintenance.js +13 -0
  12. package/dist/server/routes/maintenance.js +32 -0
  13. package/dist/server/routes/mcp.js +69 -3
  14. package/dist/server/routes/pull-requests.js +2 -1
  15. package/dist/server/routes/workspace-messages.js +22 -0
  16. package/dist/server/routes/workspaces.js +60 -4
  17. package/dist/server/services/agent/orchestrator.js +21 -6
  18. package/dist/server/services/agent/session-controller.js +4 -1
  19. package/dist/server/services/cron-service.js +7 -0
  20. package/dist/server/services/pr-checkout-rollback-service.js +51 -0
  21. package/dist/server/services/pr-checkout-service.js +21 -1
  22. package/dist/server/services/quota-backoff-service.js +7 -0
  23. package/dist/server/services/update-maintenance-service.js +44 -0
  24. package/dist/server/services/wakeup-service.js +21 -3
  25. package/dist/server/services/workspace-creation-mcp-service.js +214 -0
  26. package/dist/server/services/workspace-group-message-mcp-service.js +58 -0
  27. package/dist/server/services/workspace-group-message-service.js +182 -0
  28. package/dist/server/services/workspace-lifecycle-mcp-service.js +72 -0
  29. package/dist/server/services/workspace-message-service.js +5 -0
  30. package/dist/shared/pr-checkout-steps.js +86 -0
  31. package/dist/shared/workspace-creation-tools.js +182 -0
  32. package/dist/shared/workspace-group-message-tools.js +134 -0
  33. package/dist/shared/workspace-group-messages.js +27 -0
  34. package/dist/shared/workspace-lifecycle-tools.js +112 -0
  35. package/docker-compose.vps.yml +60 -0
  36. package/docs/deployment-vps.md +506 -0
  37. package/docs/getting-started.md +9 -0
  38. package/package.json +8 -1
  39. package/scripts/kobo-gh +9 -0
  40. package/scripts/kobo-github-app-token +190 -0
  41. package/scripts/kobo-install +392 -0
  42. package/scripts/kobo-update +499 -0
  43. package/src/client/dist/pwa/assets/ActivityFeed-S3XGw3WW.js +2 -0
  44. package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-lKjfaC6V.js → AutoLoopFinalReviewSettings-CC8ICe_I.js} +1 -1
  45. package/src/client/dist/pwa/assets/{ChangelogPage-BplM7qEB.js → ChangelogPage-C1aiPBoi.js} +1 -1
  46. package/src/client/dist/pwa/assets/{ComparisonPanel-DxK5v9w_.js → ComparisonPanel-DElbn4ac.js} +1 -1
  47. package/src/client/dist/pwa/assets/CreatePage-Bx58t4YW.js +2 -0
  48. package/src/client/dist/pwa/assets/{DashboardPage-BsP-MATS.js → DashboardPage-D1S3QGp4.js} +1 -1
  49. package/src/client/dist/pwa/assets/DiffViewer-BH4X9ru1.js +8 -0
  50. package/src/client/dist/pwa/assets/{DocumentsPanel-CGrf8Q3H.js → DocumentsPanel-C4AvyZF3.js} +1 -1
  51. package/src/client/dist/pwa/assets/{DrawerToggleButton-BANeLSsR.js → DrawerToggleButton-LzIEeeJY.js} +1 -1
  52. package/src/client/dist/pwa/assets/GitPanel-BP3N_4nh.js +3 -0
  53. package/src/client/dist/pwa/assets/{HealthPage-BVTNx-92.js → HealthPage-Cm13vxOi.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-DGmMkfuw.js +2 -0
  55. package/src/client/dist/pwa/assets/{MainLayout-C6nvIQjU.css → MainLayout-DoAEgANU.css} +1 -1
  56. package/src/client/dist/pwa/assets/{MemoryPanel-C71rTpv9.js → MemoryPanel-C6tzCv6D.js} +1 -1
  57. package/src/client/dist/pwa/assets/{MemoryProposalList-CoqhUGrv.js → MemoryProposalList-CzpzIirK.js} +1 -1
  58. package/src/client/dist/pwa/assets/{QBtnDropdown-U9Ey5W8h.js → QBtnDropdown-BnuKPevV.js} +1 -1
  59. package/src/client/dist/pwa/assets/{QExpansionItem-Be_A1MT1.js → QExpansionItem-DjaUhgjt.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QScrollArea-D4lYzAPQ.js → QScrollArea-Dpksslru.js} +1 -1
  61. package/src/client/dist/pwa/assets/QSelect-Dh4CjliE.js +1 -0
  62. package/src/client/dist/pwa/assets/{QTooltip-kStOgY0X.js → QTooltip-CH45NuVs.js} +1 -1
  63. package/src/client/dist/pwa/assets/{SchedulePanel-C7nTGdWE.js → SchedulePanel-QAjXuTIN.js} +1 -1
  64. package/src/client/dist/pwa/assets/{SearchPage-C6J72pkQ.js → SearchPage-B1pbwDej.js} +1 -1
  65. package/src/client/dist/pwa/assets/SettingsPage-CUQefsDh.js +9 -0
  66. package/src/client/dist/pwa/assets/{SplitWorkspacePage-Bqn4gdVw.js → SplitWorkspacePage-CJ7e-KPh.js} +1 -1
  67. package/src/client/dist/pwa/assets/{TerminalPanel-zLeDCReI.js → TerminalPanel-DVomz3oo.js} +10 -10
  68. package/src/client/dist/pwa/assets/{ToolCallItem-B8b_Sagj.js → ToolCallItem-BntDHc6p.js} +3 -3
  69. package/src/client/dist/pwa/assets/{TouchPan-Bl8VzIDK.js → TouchPan-DwqfsAxH.js} +1 -1
  70. package/src/client/dist/pwa/assets/{TourReplayButton-pL0HCboU.js → TourReplayButton-OIavXdUg.js} +1 -1
  71. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-9Eomx0rQ.js → WorkspaceAttentionLabels-CYwN1-Rf.js} +1 -1
  72. package/src/client/dist/pwa/assets/WorkspacePage-CAsY7Hl-.js +4 -0
  73. package/src/client/dist/pwa/assets/{browser-C-PJ4alw.js → browser-Cl0d1gbu.js} +1 -1
  74. package/src/client/dist/pwa/assets/{build-path-tree-SW9rj7B_.js → build-path-tree-J3YQeMuV.js} +1 -1
  75. package/src/client/dist/pwa/assets/{custom-sounds-toz-4drX.js → custom-sounds-D-T5rpgv.js} +1 -1
  76. package/src/client/dist/pwa/assets/de-C-RtXLqL.js +17 -0
  77. package/src/client/dist/pwa/assets/{editor.api-BJTPhnh8.js → editor.api-BoFiCjBZ.js} +1 -1
  78. package/src/client/dist/pwa/assets/{efforts-BO9xGn9q.js → efforts-Y6XotW97.js} +1 -1
  79. package/src/client/dist/pwa/assets/es-CBlW91ZK.js +17 -0
  80. package/src/client/dist/pwa/assets/{expand-template-BOZMtWau.js → expand-template-sfOHWudd.js} +1 -1
  81. package/src/client/dist/pwa/assets/{format-hEnpRyit.js → format-mWov137C.js} +1 -1
  82. package/src/client/dist/pwa/assets/{formatters-DD_eFc4z.js → formatters-B_XsveM0.js} +1 -1
  83. package/src/client/dist/pwa/assets/fr-Hfzp_sF5.js +17 -0
  84. package/src/client/dist/pwa/assets/{freemarker2-B6nUofvT.js → freemarker2-2pGNakhH.js} +1 -1
  85. package/src/client/dist/pwa/assets/{handlebars-4P3gtM8p.js → handlebars-Bn2V6Liw.js} +1 -1
  86. package/src/client/dist/pwa/assets/{html-C9ZzWo-y.js → html-BBhkJkf2.js} +1 -1
  87. package/src/client/dist/pwa/assets/i18n-sBCWwf8Y.js +1 -0
  88. package/src/client/dist/pwa/assets/index-DCpe-Tfm.js +20 -0
  89. package/src/client/dist/pwa/assets/it-Bl-v27qg.js +17 -0
  90. package/src/client/dist/pwa/assets/{javascript-BGEC5ggd.js → javascript-Ba0G68U2.js} +1 -1
  91. package/src/client/dist/pwa/assets/{liquid-DNtF630O.js → liquid-C5VrzHr_.js} +1 -1
  92. package/src/client/dist/pwa/assets/{mdx-CICwyOd0.js → mdx-DrPRiuKb.js} +1 -1
  93. package/src/client/dist/pwa/assets/{monaco-contributions-BkuwvgrT.js → monaco-contributions-qQUY6VS7.js} +1 -1
  94. package/src/client/dist/pwa/assets/{monaco.contribution-GX8XA-PE.js → monaco.contribution-Dtq2RsyB.js} +2 -2
  95. package/src/client/dist/pwa/assets/network-auth-BPJpXK4L.js +1 -0
  96. package/src/client/dist/pwa/assets/notify-theme-Bl9iRsZB.js +1 -0
  97. package/src/client/dist/pwa/assets/{project-color-B1SMFjSe.js → project-color-Dzvvmaml.js} +1 -1
  98. package/src/client/dist/pwa/assets/{python-n5uPdTVS.js → python-Cuf_7umq.js} +1 -1
  99. package/src/client/dist/pwa/assets/{razor-AOj0nsUt.js → razor-DcVpxFeh.js} +1 -1
  100. package/src/client/dist/pwa/assets/{typescript-Uhkrqihh.js → typescript-Bjcc3kYC.js} +1 -1
  101. package/src/client/dist/pwa/assets/{use-search-index-status-CW0BdpBZ.js → use-search-index-status-Bwf40BEI.js} +1 -1
  102. package/src/client/dist/pwa/assets/use-tours-CWafZt-m.js +2 -0
  103. package/src/client/dist/pwa/assets/{use-worktree-restore-BZxibZS5.js → use-worktree-restore-DkcM2FAZ.js} +1 -1
  104. package/src/client/dist/pwa/assets/{workspace-templates-W557e1-m.js → workspace-templates-DL_Cs7mD.js} +1 -1
  105. package/src/client/dist/pwa/assets/{xml-B-hnMgEs.js → xml-BmJwqJOz.js} +1 -1
  106. package/src/client/dist/pwa/assets/{yaml-DqXkTvgm.js → yaml-DAssfLhS.js} +1 -1
  107. package/src/client/dist/pwa/index.html +1 -1
  108. package/src/client/dist/pwa/sw.js +1 -1
  109. package/src/mcp-server/README.md +130 -12
  110. package/src/mcp-server/kobo-tasks-server.ts +27 -105
  111. package/src/mcp-server/workspace-dialogue-client.ts +18 -3
  112. package/src/client/dist/pwa/assets/ActivityFeed-D7jouiuP.js +0 -2
  113. package/src/client/dist/pwa/assets/CreatePage-D5ilP5kt.js +0 -2
  114. package/src/client/dist/pwa/assets/DiffViewer-Cd15vFWj.js +0 -8
  115. package/src/client/dist/pwa/assets/GitPanel-CEPV5Qw0.js +0 -3
  116. package/src/client/dist/pwa/assets/MainLayout-DBKtwilW.js +0 -2
  117. package/src/client/dist/pwa/assets/QSelect-fS0DLOd1.js +0 -1
  118. package/src/client/dist/pwa/assets/SettingsPage-SuH-aIzv.js +0 -9
  119. package/src/client/dist/pwa/assets/WorkspacePage-B6clgURl.js +0 -4
  120. package/src/client/dist/pwa/assets/de-Ddxe8cc6.js +0 -17
  121. package/src/client/dist/pwa/assets/es-DBJIQeWW.js +0 -17
  122. package/src/client/dist/pwa/assets/fr-yBzIIPaQ.js +0 -17
  123. package/src/client/dist/pwa/assets/i18n-2-ULabk2.js +0 -1
  124. package/src/client/dist/pwa/assets/index-Vk99jtxD.js +0 -20
  125. package/src/client/dist/pwa/assets/it-D44U-PT1.js +0 -17
  126. package/src/client/dist/pwa/assets/network-auth-KnWzjYIc.js +0 -1
  127. package/src/client/dist/pwa/assets/notify-theme-CWuZTz4Z.js +0 -1
  128. package/src/client/dist/pwa/assets/use-tours-CndFlyL-.js +0 -2
package/AGENTS.md CHANGED
@@ -41,6 +41,7 @@ npm run typecheck:tests # type check backend test fixtures and tests
41
41
  npm test # backend vitest suite
42
42
  (cd src/client && npm test) # client stores, utilities, composables and components
43
43
  npm run test:all # backend + client suites
44
+ npm run test:deployment # Python 3.10+ / OpenSSL tooling tests; no Docker/network/production data
44
45
  make ci # install, audit, lint, type checks, build, both suites
45
46
  npm run test:watch # vitest in watch mode
46
47
 
@@ -124,6 +125,7 @@ src/
124
125
  | `session_event_metrics` / `usage_snapshots` | session event counters and provider usage snapshots |
125
126
  | `workspace_activity` | significant lifecycle/PR events for the absence digest |
126
127
  | `search_*` | derived logical-message search index and indexing progress; see `search-schema.ts` |
128
+ | `workspace_message_batches` | immutable recipient snapshots, request fingerprint and per-recipient delivery outcomes (v53); no stored message body |
127
129
  | `mcp_message_requests` | durable external-message idempotency ledger; independent of event retention |
128
130
  | `schema_migrations` | applied migration versions, names and timestamps |
129
131
  | `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 |
@@ -211,6 +213,8 @@ Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `tas
211
213
 
212
214
  ### External MCP dialogue and update checks
213
215
 
216
+ Workspace-bound stdio, global stdio and HTTP expose shared `create_workspace` / `diagnose_workspace_pr` definitions in `src/shared/workspace-creation-tools.ts`. Stdio forwards them to the backend. `workspace-creation-mcp-service.ts` validates inputs and dispatches through the existing workspace and PR routes in process; preserve UI validation, checkout fingerprints, lifecycle guards and rollback. Inputs use snake_case except the existing camelCase PR checkout decisions. Attachments are inline base64, bounded to the shared 10-file/50 MiB decoded limit before checkout mutations. Keep SDK HTTP/stdio limits aligned with the shared creation envelope budget. Only creation envelopes get the larger base64 HTTP body budget; ordinary MCP requests remain 1 MiB. After a known PR creation failure, rollback only a checkout proven newly created, unchanged, clean (including ignored files) and unowned, under the Git repository lock; retain other checkouts and expose recovery details. Do not retry uncertain creation automatically or treat `creation_id` as an idempotency key. Restricted review/handoff launches cannot create workspaces or invoke lifecycle mutations. Shared lifecycle tools (`workspace-lifecycle-tools.ts` / `workspace-lifecycle-mcp-service.ts`) also expose archive, purge, delete, unarchive and restore through the same transports. Reject reserved/bulk/path-like IDs, require explicit purge/delete flags, and preserve warnings. Single-workspace DELETE accepts optional `confirmationBranch`, rechecked under its lifecycle guard before stopping processes; MCP always supplies it. Branch rename/resync share this guard so the confirmed branch and checkout path cannot change during teardown. Recovery uses `restore-worktree` before unarchive and never starts an agent.
217
+
214
218
  `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.
215
219
 
216
220
  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.
@@ -245,6 +249,57 @@ Two emit flavors in `websocket-service.ts`:
245
249
 
246
250
  ## External integrations
247
251
 
252
+ ### VPS deployment tooling
253
+
254
+ `docker-compose.vps.yml` is the standalone fresh-instance example for an existing
255
+ Traefik network. It persists `/root`, mounts project paths identically on host and
256
+ container, and leaves the Docker socket opt-in. `docs/deployment-vps.md` owns the
257
+ operator walkthrough; `CONFIGURATION.md` compares all Compose topologies.
258
+
259
+ `scripts/kobo-install` guides a fresh installation; it does not adopt existing
260
+ unmanaged stacks. Preserve its read-only `--check`, explicit Docker-socket opt-in,
261
+ managed-file fingerprints, shared updater lock, and durable start phases. Completed
262
+ reruns must never restart containers or replace credentials. Only the updater's
263
+ image pin may differ from the managed Compose fingerprint. Interrupted starts
264
+ require diagnosis instead of automatic recreation. This tool reuses the updater's
265
+ release resolution/build helpers; no separate public image registry is assumed.
266
+
267
+ Optional GitHub App authentication uses `scripts/kobo-gh` and
268
+ `scripts/kobo-github-app-token`, mounted read-only with the user's RSA key. The
269
+ wrapper honors explicit `GH_TOKEN` / `GITHUB_TOKEN`, otherwise obtains a renewable
270
+ installation token from the private, locked cache. Keep JWTs, PEM contents and
271
+ tokens out of logs/errors. HTTPS Git uses the same wrapper via a github.com-scoped
272
+ credential helper; commit author identity is configured separately. Never bake
273
+ credentials into an image or add web settings for these host-managed secrets.
274
+
275
+ `scripts/kobo-update` is a Python 3.10+ operator tool for this layout, separate
276
+ from the backend's update notifications. Preserve explicit invocation, idle checks,
277
+ unique image tags, private stopped-volume backups, migration tests on a copy,
278
+ and durable failure records. The authenticated `/api/maintenance/update` reservation
279
+ checks runtime ownership and closes agent admission synchronously before shutdown.
280
+ Track in-flight workspace creations and PR checkouts through all asynchronous
281
+ steps and rollback. Maintenance and creation admission must exclude each other
282
+ synchronously; rejecting only the final agent start is too late. Direct HTTP and
283
+ MCP creation paths share this guard.
284
+ Scheduled cron/wakeup/quota deliveries defer without consuming persisted intent;
285
+ valid pending wakeups get `retry_at=target_at` before successful reservation (only
286
+ when `retry_at` was null), so long maintenance does not expire them at boot. Keep
287
+ the requested deadline/prompt and existing retry metadata; do not revive already
288
+ stale, never-attempted wakeups. New wakeups scheduled during maintenance get the
289
+ same durable deferral marker.
290
+ Replaced timers must invalidate deferred callbacks. Legacy runtimes without this
291
+ protocol refuse automatic deployment while `--prepare` remains available. Failed
292
+ pre-stop requests release their reservation; uncertain stops must be confirmed
293
+ before restarting. A crashed updater may require the documented manual recovery. Never automatically run an old image against a
294
+ potentially migrated database or restore snapshots after new work may have run.
295
+ No deployment-specific hostname, App credentials or private source paths belong
296
+ in this script. Its deterministic tests run through `npm run test:deployment`,
297
+ `make ci` and the PR workflow; live deployment is not a CI test.
298
+ Installer/updater Compose commands explicitly select the stack's `.env` and strip
299
+ the template's Kobo/Traefik interpolation variables plus `COMPOSE_*` from a copy
300
+ of the child environment. Apply the same policy to captured and logged commands;
301
+ never mutate the parent environment or lose Docker/forge authentication settings.
302
+
248
303
  ### Forge providers
249
304
 
250
305
  `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.
@@ -527,6 +582,12 @@ unread events or deleting history; re-enabling resumes from the saved checkpoint
527
582
 
528
583
  `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.
529
584
 
585
+ ### Group workspace messages
586
+
587
+ `workspace-group-message-service.ts` owns durable group receipts and a server-wide queue capped at three simultaneous delivery attempts. `/api/workspace-messages` creates a batch (202); `/:id` reads its receipt. Requests contain explicit unique workspace IDs, one content body and a stable request ID. A repeated ID is observation only; different content/recipients/source conflicts. Tags/status filters are preview helpers (OR within each group, AND across groups); archived/purged workspaces cannot receive messages. The optional `devServerRunning` preview filter (`dev_server_running` in MCP) requires exact dev-server status `running` and intersects tags/statuses. The dialog prefers the live dev-server store status over the workspace snapshot. Both UI tag filters union the global settings catalogue with assigned tags via `collectTags`; unused catalogue tags have count zero. Wait for settings and archived workspaces to load before pruning a remembered sidebar selection.
588
+
589
+ Snapshot each target and its delivery mode before scheduling: auto-loop uses `next_iteration`, other workspaces normal chat. Preserve common lifecycle, human-input and compaction guards; a mode change rejects instead of silently changing behavior. For an existing controller, run `beforeDispatch` after its startup promise settles and immediately before engine delivery, so a mode change during startup is still rejected. Receipt completion means accepted/queued, not an agent reply. Persist chat event and accepted outcome transactionally; failed notification cannot undo acceptance. Startup marks pending targets `not_sent` and in-flight ones `unknown`, never replays them. Shutdown stops scheduling before draining active attempts and closing SQLite. Migration v53 and fresh schema share `initWorkspaceMessageBatchSchema`. The dialog stores its immutable request in per-tab sessionStorage for reload recovery and never POSTs automatically on reload. Keep all MCP transports on the same backend owner, launch read-only restrictions and own-workspace exclusion.
590
+
530
591
  ### Bulk workspace info refresh
531
592
 
532
593
  `GET /api/workspaces/info` returns `{ workspaces, prSnapshots, gitStats }` in one shot. The client polls this endpoint every 15 s so every non-archived workspace stays ≤ 15 s fresh without a per-card stats request. The poll is skipped while the browser tab is hidden and fires once immediately when the tab becomes visible again. The server-side pr-watcher feeds the same caches (`lastKnownGitStats` map, PR snapshots) so the work is shared between the polling client and the watchdog loop.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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
+ ## 2.1.18
8
+
9
+ - fix(tests): resolve deployment paths on macos
10
+
11
+ ## 2.1.17
12
+
13
+ - feat(kobo): add VPS tooling and touch drawer resizing
14
+
15
+ ## 2.1.16
16
+
17
+ - feat(workspaces): add group messaging and expand mcp lifecycle tools
18
+
7
19
  ## 2.1.15
8
20
 
9
21
  - feat(auto-loop): add durable final reviews and correction cycles
package/CONFIGURATION.md CHANGED
@@ -35,6 +35,8 @@ Reference for Kōbō configuration and external integrations. The tables cover t
35
35
  - [Quick local test — docker-compose.local.yml](#quick-local-test--docker-composelocalyml)
36
36
  - [Rehearsing the reverse-proxy stack — docker-compose.local-traefikyml](#rehearsing-the-reverse-proxy-stack--docker-composelocal-traefikyml)
37
37
  - [Real deployment — docker-compose.exampleyml](#real-deployment--docker-composeexampleyml)
38
+ - [VPS with existing Traefik](#vps-with-existing-traefik)
39
+ - [Docker updates and backups](#docker-updates-and-backups)
38
40
  - [Troubleshooting](#troubleshooting)
39
41
  - [Agent runtimes](#agent-runtimes)
40
42
  - [Claude Code](#claude-code)
@@ -255,6 +257,12 @@ When working from source, `npm run install-all` installs the root, client, and
255
257
  build-only Workbox dependencies and is required by `npm run build`, but not by
256
258
  the normal development client.
257
259
 
260
+ On touch screens, drag the inner edge of either side panel to resize it. The
261
+ horizontal divider inside the right panel can also be dragged. Widths and the
262
+ split position are remembered in this browser; on small screens, a strip of the
263
+ chat remains visible so you can tap outside a panel to close it. Sidebar actions
264
+ wrap onto additional lines when needed, keeping Settings and Create accessible.
265
+
258
266
  ## Custom change-source-branch script
259
267
 
260
268
  The **Change source branch** action uses the effective `changeSourceBranchScript`:
@@ -502,6 +510,18 @@ Two consequences worth knowing:
502
510
  - However far behind the loop falls (a laptop waking from sleep, say), it sends
503
511
  **one** reminder per pass, not one per missed interval.
504
512
 
513
+ ## Group workspace messages
514
+
515
+ Use **Group message** in the workspace drawer to send the same instruction to several workspaces. Combine tags and workspace statuses, review the named recipient list, and deselect individual workspaces before sending. Selected tags match any tag; selected statuses match any status; both filter groups must match. Enable **Only workspaces with a running development server** to further restrict the list to server status `running`; starting, stopping, stopped, unknown and failed servers are excluded. This filter updates with received server status changes. Empty filters include all non-archived, non-purged workspaces. Both the group-message tag selector and the sidebar tag filter include the saved global tag catalogue plus tags already assigned to workspaces. Unused catalogue tags remain visible (with a zero count in the sidebar); assign a tag through a workspace menu before using it to target that workspace.
516
+
517
+ Auto-loop recipients receive a durable instruction for their next iteration, without stopping the loop. Other recipients use normal chat delivery, which can resume an inactive agent. Normal chat restrictions still apply: pending human questions, compaction and lifecycle operations are not bypassed. The delivery mode is fixed when submitted; a subsequent auto-loop mode change rejects that recipient rather than silently switching modes.
518
+
519
+ Each batch accepts up to 200 recipients and 100,000 characters of plain text. Delivery runs on the server with at most three attempts in flight. Results distinguish sent, queued for auto-loop, rejected, not sent and uncertain delivery. **Queued** means saved for an upcoming iteration, not already processed by the agent. Failures do not stop delivery to the other recipients.
520
+
521
+ Closing the dialog or disconnecting the browser does not cancel an accepted batch. The browser tab retains the request ID and original payload across reloads to recover its receipt, independently of other tabs. **Check this delivery** reuses that request and cannot duplicate accepted deliveries. After a server restart, unstarted recipients become **Not sent**, and interrupted attempts become **Delivery uncertain**; they are never resent automatically. Inspect the affected workspace's chat before creating a new message for an uncertain result.
522
+
523
+ The same feature is available through internal/global stdio and external HTTP MCP; see the [MCP guide](src/mcp-server/README.md). Batch receipts retain recipient names, IDs, modes and outcomes independently of workspace deletion. The batch table stores a content fingerprint, not the message body; accepted messages retain normal workspace chat history.
524
+
505
525
  ## Attachments in the creation description
506
526
 
507
527
  On the create page, paste or drop files into the mission description, or use
@@ -827,7 +847,7 @@ The setting lives in `settings.json` under `global` and has no UI control yet:
827
847
 
828
848
  ## External LLM access through MCP
829
849
 
830
- An external MCP client can connect to `http://127.0.0.1:3000/api/mcp` using Streamable HTTP (use your backend's configured port). It can discover workspaces, read their conversations, send messages to their agents and answer pending questions. The existing global stdio MCP server exposes these conversation tools too.
850
+ An external MCP client can connect to `http://127.0.0.1:3000/api/mcp` using Streamable HTTP (use your backend's configured port). It can create workspaces with the same options as the form, diagnose and check out existing PRs, discover workspaces, read their conversations, send messages and answer pending questions. Workspace-bound stdio, global stdio and HTTP share the creation contract, including brainstorm settings, final auto-loop review, workflow policy, imports, existing worktrees, comparison grouping and inline attachments. All three transports also expose archive, disk-space purge, permanent deletion with explicit branch confirmation, unarchive and recovery of purged worktrees. See the [creation and PR guide](src/mcp-server/README.md#create_workspace) for fields, explicit checkout decisions and timeout/retry behavior.
831
851
 
832
852
  Remote HTTP clients follow the [network access](#network-access) settings and supply the Kōbō token as `Authorization: Bearer <token>` or `X-Kobo-Token`. The endpoint uses the same Host/Origin and reverse-proxy policy as the rest of the backend. No network setting is changed automatically.
833
853
 
@@ -1096,27 +1116,31 @@ For the concrete Docker setup (which compose file, which env vars, which volumes
1096
1116
 
1097
1117
  ## Docker deployment
1098
1118
 
1099
- Kōbō ships an official `Dockerfile` and three ready-to-use Compose files, one per use case. All
1100
- three build from the same image; only the topology and a handful of env vars differ.
1119
+ Kōbō ships an official `Dockerfile` and four Compose examples. Choose whether you
1120
+ want to reuse host credentials or start an independent instance, and whether
1121
+ Traefik already exists. Build the image from a source checkout before starting.
1101
1122
 
1102
1123
  ### Choosing a compose file
1103
1124
 
1104
1125
  | File | Topology | Use it for |
1105
1126
  |---|---|---|
1106
- | [`docker-compose.local.yml`](./docker-compose.local.yml) | Kōbō only, port `3000` published directly. No Traefik, no TLS, no SSH server, no Docker socket. | Trying Kōbō out or poking at a change locally — the fastest path to a running instance. |
1127
+ | [`docker-compose.local.yml`](./docker-compose.local.yml) | Kōbō only, port `3000` published directly. No Traefik, TLS, published SSH port or Docker socket. | Trying Kōbō out or poking at a change locally — the fastest path to a running instance. |
1107
1128
  | [`docker-compose.local-traefik.yml`](./docker-compose.local-traefik.yml) | Traefik in front, Kōbō never on a published port, `kobo.localhost` (auto-resolves to `127.0.0.1`, no `/etc/hosts` editing). | Rehearsing the exact reverse-proxy topology/auth/mount setup locally, before touching a real VPS. |
1129
+ | [`docker-compose.vps.yml`](./docker-compose.vps.yml) | Kōbō only, existing external Traefik network, persistent `/root`, no published ports or host credential mounts. | A fresh, independent remote instance; follow the [VPS guide](./docs/deployment-vps.md). |
1108
1130
  | [`docker-compose.example.yml`](./docker-compose.example.yml) | Traefik + Let's Encrypt (real domain, real TLS), SSH access, optional Docker-socket passthrough. | The reference for an actual production deployment (VPS behind a reverse proxy). |
1109
1131
 
1110
- All three share the same underlying image (`docker build -t kobo:local .` from the repo root,
1111
- once) and the same core volume set; they differ in network topology and which optional pieces
1112
- (SSH server, Docker socket, TLS) are wired in.
1132
+ All four use the same underlying image (`docker build -t kobo:local .` from the
1133
+ repo root). The original three mount provider/Git configuration from the host;
1134
+ the VPS example keeps independent credentials in a persistent container home.
1135
+ The VPS example overrides the entrypoint to start Node directly; the original
1136
+ files use the image entrypoint, which starts `sshd` even when port 22 is not published.
1113
1137
 
1114
1138
  ### The image
1115
1139
 
1116
1140
  Multi-stage `Dockerfile`, `node:24.19.0-slim` base:
1117
1141
 
1118
1142
  - **Build stage**: installs root, client, and PWA deps, then runs `npm run build` (client via Quasar, server via `tsc`).
1119
- - **Runtime stage**: production deps only, plus OS tooling the agents and forge CLIs need — `git`, `openssh-client` + `openssh-server`, `ripgrep`, `fd-find`, `jq`, `curl`, Python + build tools, the Docker CLI (talks to a mounted host socket, see [Volumes](#volumes)), `gh`, `glab`, `tmux`, `vim`/`nano`, and more for interactive shell use.
1143
+ - **Runtime stage**: production deps only, plus OS tooling the agents and forge CLIs need — `git`, `openssh-client` + `openssh-server`, `ripgrep`, `fd-find`, `jq`, `curl`, Python + build tools, the Docker CLI and Compose plugin (`docker compose`, talks to a mounted host socket, see [Volumes](#volumes)), `gh`, `glab`, `tmux`, `vim`/`nano`, and more for interactive shell use.
1120
1144
  - **Global CLIs**: `@anthropic-ai/claude-code` and `@openai/codex` are installed globally for interactive login (`claude /login`, `codex login` over SSH) — separate from Kōbō's own bundled SDK/subprocess usage, which needs no global install.
1121
1145
  - **`git config --system --add safe.directory '*'`**: the container runs as root, but bind-mounted project repos keep the *host* user's UID on disk. Git's ownership check (CVE-2022-24765 mitigation) refuses to touch a repo it doesn't "own" unless told otherwise. This line pre-trusts every mounted path — fine for a single-user dev container, not something you'd do on a multi-tenant host.
1122
1146
  - **`HEALTHCHECK`**: `curl -f http://localhost:$SERVER_PORT/api/health` every 30s. `/api/health` is exempt from the network-access token gate unconditionally (even with `networkAccessBehindProxy` on) — Docker's `HEALTHCHECK` process has no way to supply the runtime-generated token, and without the exemption a "behind a reverse proxy" container never reports healthy, which makes Traefik's Docker provider filter it out of its router table entirely (Traefik drops unhealthy/starting containers from routing).
@@ -1130,11 +1154,13 @@ Beyond the [general environment variables](#environment-variables), these matter
1130
1154
 
1131
1155
  | Variable | Where it's set | Purpose |
1132
1156
  |---|---|---|
1133
- | `KOBO_HOME` | all three compose files | Points at the named volume mount (`/root/.config/kobo`) so state survives `docker compose down`. |
1134
- | `KOBO_NETWORK_ACCESS_ENABLED` | all three | Binds Kōbō to `0.0.0.0` inside the container at boot — required, since a Docker-forwarded or Traefik-proxied connection never looks like true loopback from Kōbō's point of view, even locally. Without it the container's `127.0.0.1`-only default blocks every external route. |
1135
- | `KOBO_NETWORK_ACCESS_BEHIND_PROXY` | `local-traefik.yml`, `example.yml` | Disables the loopback-trust bypass entirely — needed whenever a reverse proxy sits in front, since every request now looks like it comes from the proxy. Not set in `local.yml`, which publishes Kōbō's port directly with no proxy in between. |
1136
- | `GH_TOKEN` / `GITLAB_TOKEN` | `example.yml` (bring your own via `.env`) | Non-interactive auth for `gh`/`glab` inside the container — there's no browser to complete an interactive `gh auth login` on a headless VPS. |
1137
- | `KOBO_PROJECTS_DIR` | host-side, read by all three via `${KOBO_PROJECTS_DIR:-./kobo-projects}` | Not a container env var — a *host* shell variable that picks the bind-mount source for `/projects`. Defaults to `./kobo-projects` next to the compose file. |
1157
+ | `KOBO_HOME` | all four Compose files | Kōbō state at `/root/.config/kobo`, inside a named volume; survives `docker compose down` without `-v`. The VPS example persists all of `/root`. |
1158
+ | `KOBO_NETWORK_ACCESS_ENABLED` | all four | Binds Kōbō to `0.0.0.0` inside the container at boot — required, since a Docker-forwarded or Traefik-proxied connection never looks like true loopback from Kōbō's point of view, even locally. Without it the container's `127.0.0.1`-only default blocks every external route. |
1159
+ | `KOBO_NETWORK_ACCESS_BEHIND_PROXY` | `local-traefik.yml`, `example.yml`, `vps.yml` | Disables the loopback-trust bypass entirely — needed whenever a reverse proxy sits in front, since every request now looks like it comes from the proxy. Not set in `local.yml`, which publishes Kōbō's port directly with no proxy in between. |
1160
+ | `GH_TOKEN` / `GITLAB_TOKEN` | `example.yml` (bring your own via `.env`) | Optional non-interactive auth for `gh`/`glab` inside the container. A CLI browser/device flow can also be completed from your PC. Expiring GitHub App tokens need a [renewable helper](./docs/deployment-vps.md#github-app-bot-authentication), not a static token in `.env`. |
1161
+ | `KOBO_PROJECTS_DIR` | Compose interpolation on the host | Original three: source for `/projects`, default `./kobo-projects`. VPS: required existing absolute directory, mounted at the **same path** in the container for host-Docker bind mounts. |
1162
+ | `KOBO_DOMAIN` | `vps.yml`, Compose interpolation | Public hostname used in the Traefik rule and `KOBO_NETWORK_ACCESS_PROXY_HOST`; required, without `https://`. |
1163
+ | `TRAEFIK_NETWORK` / `TRAEFIK_ENTRYPOINT` / `TRAEFIK_CERTRESOLVER` | `vps.yml`, Compose interpolation | Existing Traefik names; defaults `web` / `websecure` / `letsencrypt`. Adapt to your proxy. |
1138
1164
 
1139
1165
  Both `KOBO_NETWORK_ACCESS_ENABLED` and `KOBO_NETWORK_ACCESS_BEHIND_PROXY` are read once at boot —
1140
1166
  this is what lets the container come up fully configured with no manual Settings-UI step (there's
@@ -1145,14 +1171,15 @@ retrieved once — see [Token / login flow](#token--login-flow).
1145
1171
 
1146
1172
  | Mount | RO/RW | Purpose |
1147
1173
  |---|---|---|
1174
+ | `kobo-home:/root` (`vps.yml`, named volume) | RW | Independent Kōbō, Claude/Codex, Git and forge state. Authenticate inside the container; no PC credential files are required. |
1148
1175
  | `<name>-data:/root/.config/kobo` (named volume) | RW | Kōbō's own state — SQLite DB, `settings.json`, etc. Named (not bind-mounted) so it survives `docker compose down` without `-v`, and so it can be deliberately **shared** across compose files by reusing the same volume name (see the two local files, both `kobo-test-data`). |
1149
- | `${KOBO_PROJECTS_DIR:-./kobo-projects}:/projects` | RW | The repos you create workspaces against, and their worktrees — all on one filesystem, so a worktree created next to a project lands on the same volume as the project itself. |
1176
+ | `${KOBO_PROJECTS_DIR:-./kobo-projects}:/projects` | RW | Your project repositories. Ensure the configured worktree directory is also persistent. For project Docker bind mounts, use the VPS example’s identical host/container paths; see the [path setup](./docs/deployment-vps.md#optional-project-docker-environments). |
1150
1177
  | `${HOME}/.ssh:/root/.ssh` | RO | Git SSH auth — needed to push/pull over SSH remotes. |
1151
1178
  | `${HOME}/.gitconfig:/root/.gitconfig` | RO | Commit author identity. Without it, agent-authored commits fail with "please tell me who you are". |
1152
1179
  | `${HOME}/.claude.json:/root/.claude.json` | RW | Claude Code auth + MCP server registrations. **Must already exist as a file on the host** (run `claude /login` locally first if it doesn't) — Docker silently creates an empty *directory* instead of erroring on a missing bind-mount source, which breaks auth in a confusing way. |
1153
1180
  | `${HOME}/.claude:/root/.claude` | RW | Claude Code's full state directory (skills/plugins cache, session transcripts, memory) — Claude Code writes into this tree during normal operation. |
1154
1181
  | `${HOME}/.codex:/root/.codex` | RW | Codex's full state directory (`auth.json`, `config.toml`). Harmless to mount even if you only use Claude Code. |
1155
- | `/var/run/docker.sock:/var/run/docker.sock` | RW, `example.yml` only | Lets the dev-server panel start/stop/tail logs for your projects' own `docker compose` stacks. **DANGER**: grants the container root-equivalent control over the host — anyone who can run a command inside `kobo` can spawn a privileged container that mounts the host filesystem. Remove this line if you don't need the dev-server panel. |
1182
+ | `/var/run/docker.sock:/var/run/docker.sock` | RW, enabled in `example.yml`, opt-in in `vps.yml` | Lets the dev-server panel start/stop/tail logs for your projects' own `docker compose` stacks. **DANGER**: grants the container root-equivalent control over the host — anyone who can run a command inside `kobo` can spawn a privileged container that mounts the host filesystem. Remove this line if you don't need the dev-server panel. |
1156
1183
 
1157
1184
  ### Ports
1158
1185
 
@@ -1163,12 +1190,12 @@ retrieved once — see [Token / login flow](#token--login-flow).
1163
1190
  | `443` | `example.yml` | Traefik's TLS entrypoint (Let's Encrypt via the `le` cert resolver), routes your real domain. |
1164
1191
  | `2222 → 22` (published) | `example.yml` only | SSH into the container (`ssh -p 2222 root@<host>`), key-based auth only via the same mounted `~/.ssh`, no password login. The one exception to "no published ports" — Traefik proxies HTTP/WS, not raw TCP, so SSH needs its own path. Rebuilding the image regenerates the SSH host keys, so expect a one-time "host key changed" warning from your SSH client after a rebuild. |
1165
1192
 
1166
- Kōbō itself is **never** published directly in the two Traefik-fronted files — only Traefik is
1193
+ Kōbō itself is **never** published directly in the three Traefik-fronted files — only Traefik is
1167
1194
  reachable, matching the real production posture.
1168
1195
 
1169
1196
  ### Token / login flow
1170
1197
 
1171
- All three files pre-enable network access via env vars, so there's no interactive Settings-UI step.
1198
+ All four files pre-enable network access via env vars, so there's no interactive Settings-UI step.
1172
1199
  Retrieve the auto-generated token from the container's boot log, then bookmark the token-bearing URL:
1173
1200
 
1174
1201
  ```bash
@@ -1252,6 +1279,36 @@ Open `https://kobo.example.com/?token=<token>`. SSH access: `ssh -p 2222 root@<h
1252
1279
  only). See [Ports](#ports) and [Volumes](#volumes) above for the full rationale on the Docker socket
1253
1280
  and SSH tradeoffs before deploying this to a real machine.
1254
1281
 
1282
+ ### VPS with existing Traefik
1283
+
1284
+ Use [`docker-compose.vps.yml`](./docker-compose.vps.yml) and the
1285
+ [step-by-step VPS guide](./docs/deployment-vps.md). This variant runs only Kōbō,
1286
+ keeps agent credentials in its own persistent home and joins an existing proxy
1287
+ network. Your browser's project picker refers to paths on the VPS/container,
1288
+ not your PC. The guide covers provider login, GitHub App bots, Git HTTPS helpers
1289
+ and the host/container path requirements for project Docker environments.
1290
+
1291
+ For a fresh stack, [`scripts/kobo-install`](./scripts/kobo-install) guides domain,
1292
+ project path, existing Traefik settings and optional GitHub App credentials.
1293
+ `--check` performs read-only checks; `--prepare` builds without starting. Its
1294
+ private manifest protects managed files from accidental replacement. GitHub App
1295
+ configuration stays in private host files with read-only container mounts, outside
1296
+ web settings. See the [guided installation](./docs/deployment-vps.md#guided-installation)
1297
+ and [manual bot configuration](./docs/deployment-vps.md#github-app-bot-authentication).
1298
+
1299
+ ### Docker updates and backups
1300
+
1301
+ For the independent VPS layout, [`scripts/kobo-update`](./scripts/kobo-update)
1302
+ accepts a stable version or defaults to npm `latest`. It has `--check`, `--prepare`
1303
+ and `--directory` options. See [updater usage and recovery](./docs/deployment-vps.md#update-with-kobo-update).
1304
+ This is an operator-invoked script; the in-app update banner does not install releases.
1305
+
1306
+ A source-built image needs rebuilding for a new release; `docker compose pull`
1307
+ alone cannot update an image that only exists locally. Recreate the service
1308
+ with the same persistent volumes and credential mounts after backing up its
1309
+ state. Preserve repository/worktree data and project databases separately.
1310
+ An old image alone is not a complete rollback after database migrations.
1311
+
1255
1312
  ### Troubleshooting
1256
1313
 
1257
1314
  - **`fatal: detected dubious ownership in repository at '/projects/<name>'`** — only happens if you
package/README.md CHANGED
@@ -22,6 +22,8 @@ Kōbō (工房, "workshop") turns *one agent, one terminal* into a real workflow
22
22
  | **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. An optional [independent final review](CONFIGURATION.md#final-review-for-auto-loop) returns findings to the original session for automatic correction and repeats until none remain, with durable recovery after restart. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
23
23
  | **🔀 Create from a PR/MR, or from a ticket** | Pick an open pull/merge request from GitHub, GitLab, or Bitbucket and Kōbō resolves every local conflict for you before spinning up the workspace. Or start straight from a Notion page or a Sentry issue URL. |
24
24
 
25
+ Use [group messages](CONFIGURATION.md#group-workspace-messages) to send one instruction to selected workspaces, filtered by tags and status, from the interface or MCP. Auto-loops receive it on their next iteration; each recipient has a durable delivery result.
26
+
25
27
  ## Quick start
26
28
 
27
29
  Requires Node.js ≥ 24.15, Git, Bash and your own Claude Code **or** Codex authentication. No skill plugin or ticket integration is required. Kōbō is a local, single-user tool; each user installs their own instance.
@@ -51,7 +53,7 @@ Kōbō's production build is an installable PWA — use your browser's **Install
51
53
  - **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).
52
54
 
53
55
  - **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.
54
- - **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.
56
+ - **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). Internal and external MCP clients can create fully configured workspaces, including final auto-loop review, attachments and checkout of an existing PR. HTTP and stdio clients can also archive, purge, delete or restore workspaces, read conversations, send messages, and answer questions.
55
57
 
56
58
  ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
57
59
  - **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.
@@ -114,7 +116,9 @@ for credential priority and native-configuration scope.
114
116
 
115
117
  ### Docker
116
118
 
117
- An official `Dockerfile` and three ready-to-use Compose files ship in this repository: a quick local test stack, a Traefik-fronted local rehearsal (no domain needed), and a full VPS reference (Traefik + Let's Encrypt, SSH access, optional Docker-socket passthrough). See [`CONFIGURATION.md`](./CONFIGURATION.md#docker-deployment) for every compose file, env var, and volume mount.
119
+ An official `Dockerfile` and four Compose examples ship in this repository: a local test stack, a local Traefik rehearsal, a deployment including Traefik, and a fresh VPS instance behind an existing Traefik.
120
+
121
+ **Working remotely on your own VPS?** The [VPS deployment guide](./docs/deployment-vps.md) includes a guided `kobo-install` installer, optional renewable GitHub App authentication, persistent agent logins and Docker updates. Your browser runs on your PC; repositories and agents run on the VPS. See [`CONFIGURATION.md`](./CONFIGURATION.md#docker-deployment) for the Compose comparison and shared settings.
118
122
 
119
123
  ### Network access
120
124
 
@@ -162,4 +166,4 @@ GPL-3.0-or-later. See [`LICENSE`](./LICENSE).
162
166
 
163
167
  Report reproducible bugs and feature requests in [GitHub Issues](https://github.com/loicngr/Kobo/issues). Read [Contributing](./CONTRIBUTING.md) and the [Code of conduct](./CODE_OF_CONDUCT.md) before participating. Vulnerabilities belong in the private channel described in [Security](./SECURITY.md), not public issues. Support is maintained on a best-effort basis; no response-time commitment is implied.
164
168
 
165
- Platform validation and release blockers are recorded in the [public-readiness evidence](./docs/release/public-readiness-evidence.md). Linux is locally exercised; macOS CI, WSL and real-provider beta sessions require their own evidence. See [third-party notices](./THIRD_PARTY_NOTICES.md) for redistributed assets.
169
+ Linux is locally exercised; macOS CI, WSL and real-provider beta sessions require their own evidence. See [third-party notices](./THIRD_PARTY_NOTICES.md) for redistributed assets.
@@ -4,7 +4,10 @@ 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
6
  import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName, MEMORY_TOOL_DEFINITIONS } from '../shared/memory-tools.js';
7
+ import { isWorkspaceCreationTool, MAX_MCP_CREATION_REQUEST_BYTES, WORKSPACE_CREATION_TOOLS, } from '../shared/workspace-creation-tools.js';
7
8
  import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
9
+ import { isWorkspaceGroupMessageTool, WORKSPACE_GROUP_MESSAGE_TOOLS } from '../shared/workspace-group-message-tools.js';
10
+ import { isWorkspaceLifecycleTool, WORKSPACE_LIFECYCLE_TOOLS } from '../shared/workspace-lifecycle-tools.js';
8
11
  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';
9
12
  import { callMemoryTool, isMemoryOutputSuppressed } from './memory-client.js';
10
13
  import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
@@ -566,6 +569,9 @@ const WORKSPACE_SCOPED_TOOLS = [
566
569
  ];
567
570
  const GLOBAL_TOOLS = [
568
571
  ...WORKSPACE_DIALOGUE_TOOLS,
572
+ ...WORKSPACE_CREATION_TOOLS,
573
+ ...WORKSPACE_LIFECYCLE_TOOLS,
574
+ ...WORKSPACE_GROUP_MESSAGE_TOOLS,
569
575
  ...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
570
576
  {
571
577
  name: 'list_workspaces',
@@ -587,63 +593,6 @@ const GLOBAL_TOOLS = [
587
593
  },
588
594
  annotations: { readOnlyHint: true, openWorldHint: false },
589
595
  },
590
- {
591
- name: 'create_workspace',
592
- description: 'Create a new Kōbō workspace (git worktree + agent session), like the "Créer" button on the Create page. Requires the Kōbō backend server to be running and reachable at KOBO_BACKEND_URL. name/project_path/source_branch/working_branch are mandatory — unlike the UI, this API does not auto-derive a branch name from the workspace name.',
593
- inputSchema: {
594
- type: 'object',
595
- properties: {
596
- name: { type: 'string', description: 'Workspace display name.' },
597
- project_path: { type: 'string', description: 'Absolute path to the project on disk.' },
598
- source_branch: { type: 'string', description: 'Branch to base the new worktree on (e.g. "develop").' },
599
- working_branch: {
600
- type: 'string',
601
- description: 'New branch name for the worktree (e.g. "feature/my-thing").',
602
- },
603
- model: { type: 'string', description: 'Model id override (optional, defaults to project/global setting).' },
604
- reasoning_effort: { type: 'string', description: 'Reasoning effort override (optional).' },
605
- engine: { type: 'string', description: 'Agent engine id, e.g. "claude-code" or "codex" (optional).' },
606
- description: { type: 'string', description: 'Task description / initial brainstorming prompt (optional).' },
607
- tags: {
608
- type: 'array',
609
- items: { type: 'string' },
610
- description: 'Workspace tags to assign at creation (optional).',
611
- },
612
- tasks: { type: 'array', items: { type: 'string' }, description: 'Initial manual task titles (optional).' },
613
- acceptance_criteria: {
614
- type: 'array',
615
- items: { type: 'string' },
616
- description: 'Initial manual acceptance criteria (optional).',
617
- },
618
- agent_permission_mode: {
619
- type: 'string',
620
- enum: ['plan', 'bypass', 'strict', 'interactive'],
621
- description: 'Permission mode for the new session (optional).',
622
- },
623
- auto_loop: { type: 'boolean', description: 'Start the workspace in auto-loop mode (optional).' },
624
- auto_loop_session_mode: {
625
- type: 'string',
626
- enum: ['per_task', 'continuous'],
627
- description: "Auto-loop session mode when auto_loop is true (optional, default 'per_task').",
628
- },
629
- skip_setup_script: { type: 'boolean', description: "Skip the project's setup script (optional)." },
630
- },
631
- required: ['name', 'project_path', 'source_branch', 'working_branch'],
632
- },
633
- annotations: { destructiveHint: false, openWorldHint: false },
634
- },
635
- {
636
- name: 'archive_workspace',
637
- description: 'Archive a workspace by id, like the "Archiver" action in the workspace context menu. Requires the Kōbō backend server to be running.',
638
- inputSchema: {
639
- type: 'object',
640
- properties: {
641
- workspace_id: { type: 'string', description: 'Workspace id, from list_workspaces.' },
642
- },
643
- required: ['workspace_id'],
644
- },
645
- annotations: { destructiveHint: false, openWorldHint: false },
646
- },
647
596
  {
648
597
  name: 'stop_workspace',
649
598
  description: 'Force-stop the currently running agent session on a workspace, like the red "Arrêter" button in the chat header. Requires the Kōbō backend server to be running. Safe to call when nothing is running.',
@@ -751,8 +700,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
751
700
  : {}),
752
701
  };
753
702
  }
754
- if (WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
755
- if (workspaceId && a.workspace_id === workspaceId && name === 'send_workspace_message')
703
+ if (isWorkspaceCreationTool(name) ||
704
+ isWorkspaceLifecycleTool(name) ||
705
+ isWorkspaceGroupMessageTool(name) ||
706
+ WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
707
+ if (workspaceId &&
708
+ ((a.workspace_id === workspaceId && name === 'send_workspace_message') ||
709
+ (name === 'send_workspace_group_message' &&
710
+ Array.isArray(a.workspace_ids) &&
711
+ a.workspace_ids.includes(workspaceId))))
756
712
  return fail('Use the normal conversation to communicate in your own workspace.');
757
713
  return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
758
714
  }
@@ -1025,50 +981,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1025
981
  const tag = typeof a.tag === 'string' ? a.tag : undefined;
1026
982
  return ok(listWorkspacesHandler(db, { includeArchived, tag }));
1027
983
  }
1028
- if (name === 'create_workspace') {
1029
- const workspaceName = a.name;
1030
- const projectPath = a.project_path;
1031
- const sourceBranch = a.source_branch;
1032
- const workingBranch = a.working_branch;
1033
- if (!workspaceName || !projectPath || !sourceBranch || !workingBranch) {
1034
- return fail('name, project_path, source_branch, and working_branch parameters are required');
1035
- }
1036
- try {
1037
- const created = await backendRequest('POST', '/api/workspaces', {
1038
- name: workspaceName,
1039
- projectPath,
1040
- sourceBranch,
1041
- workingBranch,
1042
- model: a.model,
1043
- reasoningEffort: a.reasoning_effort,
1044
- engine: a.engine,
1045
- description: a.description,
1046
- tags: a.tags,
1047
- tasks: a.tasks,
1048
- acceptanceCriteria: a.acceptance_criteria,
1049
- agentPermissionMode: a.agent_permission_mode,
1050
- autoLoop: a.auto_loop,
1051
- autoLoopSessionMode: a.auto_loop_session_mode,
1052
- skipSetupScript: a.skip_setup_script,
1053
- });
1054
- return ok(created);
1055
- }
1056
- catch (err) {
1057
- return fail(backendErrorMessage(err));
1058
- }
1059
- }
1060
- if (name === 'archive_workspace') {
1061
- const targetId = a.workspace_id;
1062
- if (!targetId)
1063
- return fail('workspace_id parameter is required');
1064
- try {
1065
- const result = await backendRequest('POST', `/api/workspaces/${targetId}/archive`);
1066
- return ok(result);
1067
- }
1068
- catch (err) {
1069
- return fail(backendErrorMessage(err));
1070
- }
1071
- }
1072
984
  if (name === 'stop_workspace') {
1073
985
  const targetId = a.workspace_id;
1074
986
  if (!targetId)
@@ -1087,7 +999,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1087
999
  return fail(err instanceof Error ? err.message : String(err));
1088
1000
  }
1089
1001
  });
1090
- const transport = new StdioServerTransport();
1002
+ const transport = new StdioServerTransport(process.stdin, process.stdout, {
1003
+ // Include the JSON-lines delimiter in addition to the HTTP envelope budget.
1004
+ maxBufferSize: MAX_MCP_CREATION_REQUEST_BYTES + 1,
1005
+ });
1091
1006
  server.connect(transport).catch((err) => {
1092
1007
  console.error('[kobo-tasks-server] Fatal:', err);
1093
1008
  process.exit(1);
@@ -1,15 +1,27 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
  import { isMemoryToolName, validateMemoryToolArguments } from '../shared/memory-tools.js';
4
+ import { isWorkspaceCreationTool, validateWorkspaceCreationArguments } from '../shared/workspace-creation-tools.js';
4
5
  import { validateDialogueArguments } from '../shared/workspace-dialogue-tools.js';
6
+ import { isWorkspaceGroupMessageTool, validateWorkspaceGroupMessageArguments, } from '../shared/workspace-group-message-tools.js';
7
+ import { isWorkspaceLifecycleTool, validateWorkspaceLifecycleArguments } from '../shared/workspace-lifecycle-tools.js';
5
8
  import { normalizeClientName } from '../shared/workspace-message-types.js';
6
9
  /** Stateless HTTP bridge: external stdio clients never own agent runtime state. */
7
10
  export async function callWorkspaceDialogueTool(backendUrl, name, args, token, clientName) {
8
11
  const arguments_ = isMemoryToolName(name)
9
12
  ? validateMemoryToolArguments(name, args, true)
10
- : validateDialogueArguments(name, args);
13
+ : isWorkspaceCreationTool(name)
14
+ ? validateWorkspaceCreationArguments(name, args)
15
+ : isWorkspaceLifecycleTool(name)
16
+ ? validateWorkspaceLifecycleArguments(name, args)
17
+ : isWorkspaceGroupMessageTool(name)
18
+ ? validateWorkspaceGroupMessageArguments(name, args)
19
+ : validateDialogueArguments(name, args);
11
20
  const client = new Client({ name: 'kobo-stdio-bridge', version: '1.0.0' });
12
- const signal = AbortSignal.timeout(30_000);
21
+ // Creation can include forge reads, worktree setup and user setup scripts.
22
+ // A timed-out creation is ambiguous and must never be retried automatically.
23
+ const timeout = isWorkspaceCreationTool(name) || isWorkspaceLifecycleTool(name) ? 15 * 60_000 : 30_000;
24
+ const signal = AbortSignal.timeout(timeout);
13
25
  const transport = new StreamableHTTPClientTransport(new URL(`${backendUrl.replace(/\/$/, '')}/api/mcp`), {
14
26
  requestInit: {
15
27
  signal,
@@ -23,7 +35,7 @@ export async function callWorkspaceDialogueTool(backendUrl, name, args, token, c
23
35
  });
24
36
  try {
25
37
  await client.connect(transport, { signal });
26
- return await client.callTool({ name, arguments: arguments_ }, undefined, { signal });
38
+ return await client.callTool({ name, arguments: arguments_ }, undefined, { signal, timeout });
27
39
  }
28
40
  finally {
29
41
  await client.close();
@@ -6,6 +6,7 @@ import { initReviewReturnSchema, migrateDurableReviewReturns } from './review-re
6
6
  import { initSchema } from './schema.js';
7
7
  import { initSearchSchema } from './search-schema.js';
8
8
  import { initSessionHandoffSchema } from './session-handoff-schema.js';
9
+ import { initWorkspaceMessageBatchSchema } from './workspace-message-batch-schema.js';
9
10
  /** Ordered registry of all schema migrations. Append new entries at the end. */
10
11
  export const migrations = [
11
12
  {
@@ -808,6 +809,7 @@ export const migrations = [
808
809
  initAutoLoopFinalReviewSchema(db);
809
810
  },
810
811
  },
812
+ { version: 53, name: 'workspace-group-message-receipts', migrate: initWorkspaceMessageBatchSchema },
811
813
  ];
812
814
  /** Current schema version — always equals the highest migration version. */
813
815
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -5,6 +5,7 @@ import { initMemorySchema } from './memory-schema.js';
5
5
  import { initReviewReturnSchema } from './review-return-schema.js';
6
6
  import { initSearchSchema } from './search-schema.js';
7
7
  import { initSessionHandoffSchema } from './session-handoff-schema.js';
8
+ import { initWorkspaceMessageBatchSchema } from './workspace-message-batch-schema.js';
8
9
  /** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
9
10
  export function initSchema(db) {
10
11
  db.exec(`
@@ -240,4 +241,5 @@ export function initSchema(db) {
240
241
  initAutoLoopFinalReviewSchema(db);
241
242
  initSessionHandoffSchema(db);
242
243
  initMemorySchema(db);
244
+ initWorkspaceMessageBatchSchema(db);
243
245
  }
@@ -0,0 +1,9 @@
1
+ /** Receipts survive workspace deletion; message bodies live only in normal chat/loop storage. */
2
+ export function initWorkspaceMessageBatchSchema(db) {
3
+ db.exec(`CREATE TABLE IF NOT EXISTS workspace_message_batches (
4
+ id TEXT PRIMARY KEY,
5
+ fingerprint TEXT NOT NULL,
6
+ receipt_json TEXT NOT NULL,
7
+ created_at TEXT NOT NULL
8
+ )`);
9
+ }