@loicngr/kobo 2.1.15 → 2.1.16
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 +9 -0
- package/CHANGELOG.md +4 -0
- package/CONFIGURATION.md +13 -1
- package/README.md +3 -1
- package/dist/mcp-server/kobo-tasks-server.js +19 -104
- package/dist/mcp-server/workspace-dialogue-client.js +15 -3
- package/dist/server/db/migrations.js +2 -0
- package/dist/server/db/schema.js +2 -0
- package/dist/server/db/workspace-message-batch-schema.js +9 -0
- package/dist/server/index.js +6 -0
- package/dist/server/routes/mcp.js +69 -3
- package/dist/server/routes/workspace-messages.js +22 -0
- package/dist/server/routes/workspaces.js +58 -3
- package/dist/server/services/agent/orchestrator.js +1 -2
- package/dist/server/services/agent/session-controller.js +4 -1
- package/dist/server/services/pr-checkout-rollback-service.js +51 -0
- package/dist/server/services/pr-checkout-service.js +21 -1
- package/dist/server/services/workspace-creation-mcp-service.js +198 -0
- package/dist/server/services/workspace-group-message-mcp-service.js +58 -0
- package/dist/server/services/workspace-group-message-service.js +182 -0
- package/dist/server/services/workspace-lifecycle-mcp-service.js +72 -0
- package/dist/server/services/workspace-message-service.js +5 -0
- package/dist/shared/pr-checkout-steps.js +86 -0
- package/dist/shared/workspace-creation-tools.js +182 -0
- package/dist/shared/workspace-group-message-tools.js +134 -0
- package/dist/shared/workspace-group-messages.js +27 -0
- package/dist/shared/workspace-lifecycle-tools.js +112 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-tJmJhmtl.js +2 -0
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-lKjfaC6V.js → AutoLoopFinalReviewSettings-Bbrq02Xt.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-BplM7qEB.js → ChangelogPage-BJfkAVYp.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-DxK5v9w_.js → ComparisonPanel-BfP8APKY.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-BbtNfo0x.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-BsP-MATS.js → DashboardPage-DZYsAh5y.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-4Q-GJgyA.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-CGrf8Q3H.js → DocumentsPanel-BfPlfiwU.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BANeLSsR.js → DrawerToggleButton-DG1Qjqk1.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-ECnbb_rx.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-BVTNx-92.js → HealthPage-_VXG1Zeo.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BH6grTuW.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-C6nvIQjU.css → MainLayout-DnmUxNk7.css} +1 -1
- package/src/client/dist/pwa/assets/{MemoryPanel-C71rTpv9.js → MemoryPanel-CVlvGZXZ.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-CoqhUGrv.js → MemoryProposalList-SJsnzUAM.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-U9Ey5W8h.js → QBtnDropdown-DBgGsN-D.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-Be_A1MT1.js → QExpansionItem-DKgaMJ_l.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-D4lYzAPQ.js → QScrollArea-D3OCsq3g.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-Cb_s_nox.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-kStOgY0X.js → QTooltip-X5ps8Opw.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-C7nTGdWE.js → SchedulePanel-Ww_FVlW5.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-C6J72pkQ.js → SearchPage-B87WEHf3.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BAZLGUyg.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-Bqn4gdVw.js → SplitWorkspacePage-B99AJUbG.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-zLeDCReI.js → TerminalPanel-DnA1MXwu.js} +10 -10
- package/src/client/dist/pwa/assets/{ToolCallItem-B8b_Sagj.js → ToolCallItem-2vFggaur.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-Bl8VzIDK.js → TouchPan-BSU3z0aO.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-pL0HCboU.js → TourReplayButton-DQwS2xdD.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-9Eomx0rQ.js → WorkspaceAttentionLabels-CM1tGDje.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-CQN6EKIn.js +4 -0
- package/src/client/dist/pwa/assets/{browser-C-PJ4alw.js → browser-DPsupI9Q.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-SW9rj7B_.js → build-path-tree-CbIdt-p9.js} +1 -1
- package/src/client/dist/pwa/assets/{custom-sounds-toz-4drX.js → custom-sounds-DR68eyF8.js} +1 -1
- package/src/client/dist/pwa/assets/{de-Ddxe8cc6.js → de-D3Ycvf_F.js} +2 -2
- package/src/client/dist/pwa/assets/{editor.api-BJTPhnh8.js → editor.api-DvgAtnOj.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-BO9xGn9q.js → efforts-nmxfTVQD.js} +1 -1
- package/src/client/dist/pwa/assets/es-TgeS5898.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-BOZMtWau.js → expand-template-Brn5O4a1.js} +1 -1
- package/src/client/dist/pwa/assets/{format-hEnpRyit.js → format-DfJ3Fv1i.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DD_eFc4z.js → formatters-B30bNFhq.js} +1 -1
- package/src/client/dist/pwa/assets/fr-CJafryNb.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-B6nUofvT.js → freemarker2-DnZe74tc.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-4P3gtM8p.js → handlebars-CmuMfxzO.js} +1 -1
- package/src/client/dist/pwa/assets/{html-C9ZzWo-y.js → html-BD7OJFGX.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BY4JeBjk.js +1 -0
- package/src/client/dist/pwa/assets/index-C5fAw9ll.js +20 -0
- package/src/client/dist/pwa/assets/it-C5mN8Y61.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-BGEC5ggd.js → javascript-Y-OrgcmW.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-DNtF630O.js → liquid-BqOkKePH.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-CICwyOd0.js → mdx-D-58x2h0.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-BkuwvgrT.js → monaco-contributions-BW4ZMKO6.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-GX8XA-PE.js → monaco.contribution-DJWqzNG-.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BGPRhUa6.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-DZDzfxph.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-B1SMFjSe.js → project-color-gB5yeZPN.js} +1 -1
- package/src/client/dist/pwa/assets/{python-n5uPdTVS.js → python-BkBj1wDC.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-AOj0nsUt.js → razor-DOOJ_0b8.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Uhkrqihh.js → typescript-Bheo43G5.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CW0BdpBZ.js → use-search-index-status-CBpnyV-R.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-BHNasCvg.js +2 -0
- package/src/client/dist/pwa/assets/{use-worktree-restore-BZxibZS5.js → use-worktree-restore-CgABsWno.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-W557e1-m.js → workspace-templates-BnFkigYg.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-B-hnMgEs.js → xml-vHt2BD26.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DqXkTvgm.js → yaml-BvhLPzP9.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 +130 -12
- package/src/mcp-server/kobo-tasks-server.ts +27 -105
- package/src/mcp-server/workspace-dialogue-client.ts +18 -3
- package/src/client/dist/pwa/assets/ActivityFeed-D7jouiuP.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-D5ilP5kt.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-Cd15vFWj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-CEPV5Qw0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-DBKtwilW.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-fS0DLOd1.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-SuH-aIzv.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-B6clgURl.js +0 -4
- package/src/client/dist/pwa/assets/es-DBJIQeWW.js +0 -17
- package/src/client/dist/pwa/assets/fr-yBzIIPaQ.js +0 -17
- package/src/client/dist/pwa/assets/i18n-2-ULabk2.js +0 -1
- package/src/client/dist/pwa/assets/index-Vk99jtxD.js +0 -20
- package/src/client/dist/pwa/assets/it-D44U-PT1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KnWzjYIc.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-CWuZTz4Z.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CndFlyL-.js +0 -2
package/AGENTS.md
CHANGED
|
@@ -124,6 +124,7 @@ src/
|
|
|
124
124
|
| `session_event_metrics` / `usage_snapshots` | session event counters and provider usage snapshots |
|
|
125
125
|
| `workspace_activity` | significant lifecycle/PR events for the absence digest |
|
|
126
126
|
| `search_*` | derived logical-message search index and indexing progress; see `search-schema.ts` |
|
|
127
|
+
| `workspace_message_batches` | immutable recipient snapshots, request fingerprint and per-recipient delivery outcomes (v53); no stored message body |
|
|
127
128
|
| `mcp_message_requests` | durable external-message idempotency ledger; independent of event retention |
|
|
128
129
|
| `schema_migrations` | applied migration versions, names and timestamps |
|
|
129
130
|
| `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 +212,8 @@ Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `tas
|
|
|
211
212
|
|
|
212
213
|
### External MCP dialogue and update checks
|
|
213
214
|
|
|
215
|
+
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.
|
|
216
|
+
|
|
214
217
|
`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
218
|
|
|
216
219
|
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.
|
|
@@ -527,6 +530,12 @@ unread events or deleting history; re-enabling resumes from the saved checkpoint
|
|
|
527
530
|
|
|
528
531
|
`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
532
|
|
|
533
|
+
### Group workspace messages
|
|
534
|
+
|
|
535
|
+
`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.
|
|
536
|
+
|
|
537
|
+
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.
|
|
538
|
+
|
|
530
539
|
### Bulk workspace info refresh
|
|
531
540
|
|
|
532
541
|
`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,10 @@ 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.16
|
|
8
|
+
|
|
9
|
+
- feat(workspaces): add group messaging and expand mcp lifecycle tools
|
|
10
|
+
|
|
7
11
|
## 2.1.15
|
|
8
12
|
|
|
9
13
|
- feat(auto-loop): add durable final reviews and correction cycles
|
package/CONFIGURATION.md
CHANGED
|
@@ -502,6 +502,18 @@ Two consequences worth knowing:
|
|
|
502
502
|
- However far behind the loop falls (a laptop waking from sleep, say), it sends
|
|
503
503
|
**one** reminder per pass, not one per missed interval.
|
|
504
504
|
|
|
505
|
+
## Group workspace messages
|
|
506
|
+
|
|
507
|
+
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.
|
|
508
|
+
|
|
509
|
+
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.
|
|
510
|
+
|
|
511
|
+
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.
|
|
512
|
+
|
|
513
|
+
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.
|
|
514
|
+
|
|
515
|
+
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.
|
|
516
|
+
|
|
505
517
|
## Attachments in the creation description
|
|
506
518
|
|
|
507
519
|
On the create page, paste or drop files into the mission description, or use
|
|
@@ -827,7 +839,7 @@ The setting lives in `settings.json` under `global` and has no UI control yet:
|
|
|
827
839
|
|
|
828
840
|
## External LLM access through MCP
|
|
829
841
|
|
|
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
|
|
842
|
+
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
843
|
|
|
832
844
|
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
845
|
|
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).
|
|
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
|

|
|
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.
|
|
@@ -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 (
|
|
755
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import soundsRouter from './routes/sounds.js';
|
|
|
32
32
|
import templatesRouter from './routes/templates.js';
|
|
33
33
|
import usageRoutes from './routes/usage.js';
|
|
34
34
|
import voiceRouter from './routes/voice.js';
|
|
35
|
+
import workspaceMessagesRouter from './routes/workspace-messages.js';
|
|
35
36
|
import workspaceTemplatesRouter from './routes/workspace-templates.js';
|
|
36
37
|
import workspacesRouter from './routes/workspaces.js';
|
|
37
38
|
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, resumePendingReviewReturns, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
@@ -57,6 +58,7 @@ import { startUpdateChecker, stopUpdateChecker } from './services/update-check-s
|
|
|
57
58
|
import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
58
59
|
import * as wakeupService from './services/wakeup-service.js';
|
|
59
60
|
import { handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
61
|
+
import { reconcileGroupMessageBatches, stopGroupMessageBatches } from './services/workspace-group-message-service.js';
|
|
60
62
|
import { deliverWorkspaceMessage } from './services/workspace-message-service.js';
|
|
61
63
|
import { getWorkspace } from './services/workspace-service.js';
|
|
62
64
|
import { resolveRetentionConfig } from './services/ws-events-retention-service.js';
|
|
@@ -84,6 +86,7 @@ catch (err) {
|
|
|
84
86
|
}
|
|
85
87
|
runMigrations(db);
|
|
86
88
|
reconcileMessageRequests(db);
|
|
89
|
+
reconcileGroupMessageBatches(db);
|
|
87
90
|
reconcileMemoryContextsOnStartup();
|
|
88
91
|
startSearchIndex();
|
|
89
92
|
// Event retention. OPT-IN: disabled by default, so this is a no-op until the
|
|
@@ -155,6 +158,7 @@ app.use('/api/*', networkAuthMiddleware);
|
|
|
155
158
|
app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
|
|
156
159
|
// Mount route sub-routers
|
|
157
160
|
app.route('/api/workspaces', workspacesRouter);
|
|
161
|
+
app.route('/api/workspace-messages', workspaceMessagesRouter);
|
|
158
162
|
app.route('/api/mcp', mcpRouter);
|
|
159
163
|
app.route('/api/memory', memoryRouter);
|
|
160
164
|
app.route('/api/pull-requests', pullRequestsRouter);
|
|
@@ -413,6 +417,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
413
417
|
return;
|
|
414
418
|
isShuttingDown = true;
|
|
415
419
|
stopUpdateChecker();
|
|
420
|
+
const groupMessagesStopped = stopGroupMessageBatches();
|
|
416
421
|
console.log(`\n[kobo] Received ${signal}, shutting down gracefully…`);
|
|
417
422
|
const forceExitTimer = setTimeout(() => {
|
|
418
423
|
console.error('[kobo] Graceful shutdown timed out; forcing exit');
|
|
@@ -479,6 +484,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
479
484
|
finally {
|
|
480
485
|
try {
|
|
481
486
|
const stopped = await Promise.allSettled([
|
|
487
|
+
groupMessagesStopped,
|
|
482
488
|
stopWsEventsRetention(),
|
|
483
489
|
stopSearchIndex(),
|
|
484
490
|
dailyBackupScheduler.stop(),
|
|
@@ -4,21 +4,73 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { bodyLimit } from 'hono/body-limit';
|
|
6
6
|
import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName } 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 { EXTERNAL_MCP_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 { createMcpClientContext } from '../services/mcp-client-context.js';
|
|
9
12
|
import { executeMemoryMcpTool } from '../services/memory-mcp-service.js';
|
|
13
|
+
import { executeWorkspaceCreationTool, WorkspaceCreationMcpError } from '../services/workspace-creation-mcp-service.js';
|
|
10
14
|
import { executeWorkspaceDialogueTool } from '../services/workspace-dialogue-service.js';
|
|
15
|
+
import { executeWorkspaceGroupMessageTool, WorkspaceGroupMessageMcpError, } from '../services/workspace-group-message-mcp-service.js';
|
|
16
|
+
import { executeWorkspaceLifecycleTool, WorkspaceLifecycleMcpError, } from '../services/workspace-lifecycle-mcp-service.js';
|
|
11
17
|
import { prepareMemoryToolEnvelope } from '../utils/memory-token-budget.js';
|
|
12
18
|
const app = new Hono();
|
|
13
|
-
|
|
19
|
+
// Only creation envelopes may carry the larger, base64 attachment payload.
|
|
20
|
+
// Other tools retain their existing 1 MiB limit, including malformed envelopes.
|
|
21
|
+
app.use('*', bodyLimit({ maxSize: MAX_MCP_CREATION_REQUEST_BYTES }));
|
|
22
|
+
app.use('*', async (c, next) => {
|
|
23
|
+
if (c.req.raw.body) {
|
|
24
|
+
const body = await c.req.raw.clone().arrayBuffer();
|
|
25
|
+
if (body.byteLength > 1024 * 1024) {
|
|
26
|
+
let creation = false;
|
|
27
|
+
try {
|
|
28
|
+
const envelope = JSON.parse(new TextDecoder().decode(body));
|
|
29
|
+
creation = envelope?.method === 'tools/call' && envelope?.params?.name === 'create_workspace';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* Preserve the normal body-size rejection for malformed input. */
|
|
33
|
+
}
|
|
34
|
+
if (!creation)
|
|
35
|
+
return c.json({ error: 'MCP request is too large' }, 413);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return next();
|
|
39
|
+
});
|
|
40
|
+
/** Dispatch only the supported MCP operations through the existing UI handlers. */
|
|
41
|
+
async function dispatchWorkspaceRequest(pathname, init) {
|
|
42
|
+
if (pathname === '/api/workspaces/archived')
|
|
43
|
+
throw new Error('Bulk operations are not supported by workspace MCP tools');
|
|
44
|
+
const isWorkspaceOperation = (init.method === 'POST' &&
|
|
45
|
+
(pathname === '/api/workspaces' ||
|
|
46
|
+
/^\/api\/workspaces\/[^/]+\/(unarchive|archive|purge-worktree|restore-worktree)$/.test(pathname))) ||
|
|
47
|
+
((init.method === 'GET' || init.method === 'DELETE') && /^\/api\/workspaces\/[^/]+$/.test(pathname));
|
|
48
|
+
if (isWorkspaceOperation) {
|
|
49
|
+
const { default: routes } = await import('./workspaces.js');
|
|
50
|
+
return routes.request(`http://kobo.internal${pathname.slice('/api/workspaces'.length) || '/'}`, init);
|
|
51
|
+
}
|
|
52
|
+
if (init.method === 'POST' &&
|
|
53
|
+
(pathname === '/api/pull-requests/diagnose' || pathname === '/api/pull-requests/resolve')) {
|
|
54
|
+
const { default: routes } = await import('./pull-requests.js');
|
|
55
|
+
return routes.request(`http://kobo.internal${pathname.slice('/api/pull-requests'.length)}`, init);
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Unsupported workspace operation');
|
|
58
|
+
}
|
|
14
59
|
app.all('/', async (c) => {
|
|
15
60
|
const server = new Server({ name: 'kobo-workspaces', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
16
61
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
17
62
|
sessionIdGenerator: undefined,
|
|
18
63
|
enableJsonResponse: true,
|
|
64
|
+
maxRequestBodySize: MAX_MCP_CREATION_REQUEST_BYTES,
|
|
19
65
|
});
|
|
20
66
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
21
|
-
tools: [
|
|
67
|
+
tools: [
|
|
68
|
+
...EXTERNAL_MCP_TOOLS,
|
|
69
|
+
...WORKSPACE_CREATION_TOOLS,
|
|
70
|
+
...WORKSPACE_LIFECYCLE_TOOLS,
|
|
71
|
+
...WORKSPACE_GROUP_MESSAGE_TOOLS,
|
|
72
|
+
...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
|
|
73
|
+
],
|
|
22
74
|
}));
|
|
23
75
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
24
76
|
const isMemory = isMemoryToolName(request.params.name);
|
|
@@ -26,7 +78,13 @@ app.all('/', async (c) => {
|
|
|
26
78
|
const source = createMcpClientContext(c.req.header('X-Kobo-Client-Name'), c.req.header('X-Kobo-Client-Transport') === 'stdio' ? 'stdio' : 'http', c.req.header('X-Kobo-Client-Name-Encoding'));
|
|
27
79
|
const data = isMemory
|
|
28
80
|
? executeMemoryMcpTool(request.params.name, request.params.arguments ?? {}, source)
|
|
29
|
-
:
|
|
81
|
+
: isWorkspaceCreationTool(request.params.name)
|
|
82
|
+
? await executeWorkspaceCreationTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
83
|
+
: isWorkspaceLifecycleTool(request.params.name)
|
|
84
|
+
? await executeWorkspaceLifecycleTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
85
|
+
: isWorkspaceGroupMessageTool(request.params.name)
|
|
86
|
+
? executeWorkspaceGroupMessageTool(request.params.name, request.params.arguments ?? {}, source)
|
|
87
|
+
: await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
|
|
30
88
|
if (isMemory) {
|
|
31
89
|
const bounded = prepareMemoryToolEnvelope(data, { targetTokens: 1_000, transport: 'external' });
|
|
32
90
|
if (bounded.data.memoryOutputSuppressed === true)
|
|
@@ -46,6 +104,14 @@ app.all('/', async (c) => {
|
|
|
46
104
|
};
|
|
47
105
|
}
|
|
48
106
|
catch (error) {
|
|
107
|
+
if (error instanceof WorkspaceGroupMessageMcpError) {
|
|
108
|
+
const data = { error: error.message, status: error.status };
|
|
109
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof WorkspaceCreationMcpError || error instanceof WorkspaceLifecycleMcpError) {
|
|
112
|
+
const data = { error: error.message, status: error.status, stage: error.stage, details: error.details };
|
|
113
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
114
|
+
}
|
|
49
115
|
if (isMemory) {
|
|
50
116
|
const bounded = prepareMemoryToolEnvelope({
|
|
51
117
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import { MAX_GROUP_MESSAGE_REQUEST_BYTES } from '../../shared/workspace-group-messages.js';
|
|
4
|
+
import { GroupMessageError, getGroupMessageBatch, startGroupMessageBatch, } from '../services/workspace-group-message-service.js';
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
app.use('*', bodyLimit({ maxSize: MAX_GROUP_MESSAGE_REQUEST_BYTES }));
|
|
7
|
+
app.post('/', async (c) => {
|
|
8
|
+
const body = await c.req.json().catch(() => null);
|
|
9
|
+
try {
|
|
10
|
+
return c.json(startGroupMessageBatch(body), 202);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof GroupMessageError)
|
|
14
|
+
return c.json({ error: error.message }, error.status);
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
app.get('/:id', (c) => {
|
|
19
|
+
const batch = getGroupMessageBatch(c.req.param('id'));
|
|
20
|
+
return batch ? c.json(batch) : c.json({ error: 'Group message not found' }, 404);
|
|
21
|
+
});
|
|
22
|
+
export default app;
|