@loicngr/kobo 1.7.16 → 1.7.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.
- package/AGENTS.md +35 -4
- package/CHANGELOG.md +12 -0
- package/README.md +2 -1
- package/dist/mcp-server/kobo-tasks-handlers.js +48 -5
- package/dist/mcp-server/kobo-tasks-server.js +8 -20
- package/dist/server/db/migrations.js +19 -0
- package/dist/server/db/schema.js +9 -0
- package/dist/server/routes/settings.js +2 -0
- package/dist/server/routes/workspaces.js +242 -56
- package/dist/server/services/agent/engines/claude-code/engine.js +51 -7
- package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -2
- package/dist/server/services/agent/engines/codex/options-builder.js +2 -2
- package/dist/server/services/agent/orchestrator.js +1 -0
- package/dist/server/services/change-source-branch-service.js +150 -0
- package/dist/server/services/chat-history-service.js +41 -0
- package/dist/server/services/file-editor-service.js +59 -0
- package/dist/server/services/forge/github/provider.js +121 -0
- package/dist/server/services/forge/gitlab/provider.js +178 -0
- package/dist/server/services/forge/none.js +23 -0
- package/dist/server/services/forge/registry.js +17 -0
- package/dist/server/services/forge/resolve.js +34 -0
- package/dist/server/services/forge/types.js +9 -0
- package/dist/server/services/git-stats-service.js +32 -0
- package/dist/server/services/pr-watcher-service.js +33 -3
- package/dist/server/services/settings-defaults.js +77 -0
- package/dist/server/services/settings-service.js +34 -0
- package/dist/server/utils/git-ops.js +121 -134
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-DHf6rSgl.js +8 -0
- package/src/client/dist/spa/assets/{ClosePopup-CxvZA3ft.js → ClosePopup-A-tSm4aa.js} +1 -1
- package/src/client/dist/spa/assets/{CreatePage-CdZr7f3j.js → CreatePage-DL8LTcyD.js} +1 -1
- package/src/client/dist/spa/assets/DiffViewer-C4L5y8Ho.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-CgzuQueb.js +8 -0
- package/src/client/dist/spa/assets/{HealthPage-z1uIOpYk.js → HealthPage-Zsnyyv66.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-BJmBXwYn.css → MainLayout-KEr19FOv.css} +1 -1
- package/src/client/dist/spa/assets/MainLayout-OJcuFEwx.js +37 -0
- package/src/client/dist/spa/assets/{QExpansionItem-BTd5m2yV.js → QExpansionItem-CgJQdznK.js} +1 -1
- package/src/client/dist/spa/assets/{QMenu-C2Wwwf2E.js → QMenu-NVDU7D3u.js} +1 -1
- package/src/client/dist/spa/assets/{QScrollArea-A1wI0IXU.js → QScrollArea-DFNGAP1T.js} +1 -1
- package/src/client/dist/spa/assets/{QTooltip-Bfdmzm_m.js → QTooltip-BC7PnZJ1.js} +1 -1
- package/src/client/dist/spa/assets/{SearchPage-ChmKHNKn.js → SearchPage-CpmeT5hL.js} +1 -1
- package/src/client/dist/spa/assets/{SettingsPage-BJLyYrBN.css → SettingsPage-BTGPZaqC.css} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-CKz2kdw8.js +9 -0
- package/src/client/dist/spa/assets/{TouchPan-BIE5rs7U.js → TouchPan-D0fJnlOC.js} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-CcWa3--k.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-DPj03Um2.css +1 -0
- package/src/client/dist/spa/assets/{build-path-tree-BGUV3nY1.js → build-path-tree-CyqReJkk.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-BU4X8R6a.js → cssMode-DKW40Eay.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-B4xBDzmJ.js → editor.api-cIZo-p3R.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-CSZRkloL.js → editor.main-DxYwm0in.js} +3 -3
- package/src/client/dist/spa/assets/{engineFeatures-CLOVr5b4.js → engineFeatures-vEC-j3xd.js} +1 -1
- package/src/client/dist/spa/assets/{expand-template-BxUkuL5g.js → expand-template-Bmbq9pxX.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-DRz20wAV.js → freemarker2-DYe7YniO.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-C0dsvPnC.js → handlebars-CFHnjuEe.js} +1 -1
- package/src/client/dist/spa/assets/{html-Cqvj1pWs.js → html-D_DPVIcT.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-BTHNvkm6.js → htmlMode-CULL5FkI.js} +1 -1
- package/src/client/dist/spa/assets/i18n-awaKh__J.js +1 -0
- package/src/client/dist/spa/assets/index-B2qdU9v-.js +52 -0
- package/src/client/dist/spa/assets/{javascript-C8n3U02v.js → javascript-Cj-bhbPb.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-C3AFxQ6K.js → jsonMode-CJrCPpxd.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-BuxgteGZ.js → kobo-commands-B2AhWe1S.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-C4wtUDrJ.js → liquid-B4ttnSVX.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-CaT1p1F2.js → mdx-DT9HeWQS.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-CJg5GKVf.js → monaco.contribution-Dc3R0xb9.js} +2 -2
- package/src/client/dist/spa/assets/{notifications-BC6en6Lt.js → notifications-Hq-6rEYv.js} +1 -1
- package/src/client/dist/spa/assets/{permissionModes-BQHBTBwa.js → permissionModes-BA0XHeew.js} +1 -1
- package/src/client/dist/spa/assets/{python-Cj54W2Tg.js → python-DSdYwb75.js} +1 -1
- package/src/client/dist/spa/assets/{razor-D3gJxoX_.js → razor-C-5bSEPf.js} +1 -1
- package/src/client/dist/spa/assets/{render-chat-markdown-DxEHr3lW.js → render-chat-markdown-cMOd2guW.js} +1 -1
- package/src/client/dist/spa/assets/{tsMode-B6S4PLWH.js → tsMode-Bck0IzqV.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-Ca8AEX3t.js → typescript-Daj2xIGr.js} +1 -1
- package/src/client/dist/spa/assets/{use-onboarding-CNeLPDtv.js → use-onboarding-Xp0y257M.js} +1 -1
- package/src/client/dist/spa/assets/{xml-CsKo4k8C.js → xml-DLN-RVL8.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-X5yKmi6z.js → yaml-DnDRs7J6.js} +1 -1
- package/src/client/dist/spa/index.html +2 -2
- package/src/mcp-server/kobo-tasks-handlers.ts +56 -5
- package/src/mcp-server/kobo-tasks-server.ts +8 -19
- package/src/client/dist/spa/assets/ActivityFeed-DU6lDEP0.js +0 -8
- package/src/client/dist/spa/assets/DiffViewer-DTdDcKZC.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-m801GPfI.js +0 -7
- package/src/client/dist/spa/assets/MainLayout-oJdQ-QKM.js +0 -37
- package/src/client/dist/spa/assets/SettingsPage-B59LoCos.js +0 -9
- package/src/client/dist/spa/assets/WorkspacePage-Bj1PJSWT.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-tFBswKV9.css +0 -1
- package/src/client/dist/spa/assets/i18n-D1I-Us2H.js +0 -1
- package/src/client/dist/spa/assets/index-KABmOIkF.js +0 -2
package/AGENTS.md
CHANGED
|
@@ -62,7 +62,11 @@ src/
|
|
|
62
62
|
│ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota handling, watchdog, public API
|
|
63
63
|
│ │ │ ├── session-controller.ts # lifecycle wrapper around an AgentEngine instance
|
|
64
64
|
│ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
|
|
65
|
-
│ │ │ └── engines/claude-code/ #
|
|
65
|
+
│ │ │ └── engines/{claude-code,codex}/ # per-engine adapters implementing the AgentEngine contract
|
|
66
|
+
│ │ ├── forge/ # ForgeProvider abstraction — github / gitlab / none, with registry + auto-resolve
|
|
67
|
+
│ │ ├── change-source-branch-service.ts # re-target a workspace onto a new source branch (built-in cherry-pick or custom bash)
|
|
68
|
+
│ │ ├── git-stats-service.ts # pure compute of commit/ahead-behind/diff stats + forge availability for a workspace
|
|
69
|
+
│ │ ├── settings-defaults.ts # DEFAULT_* constants for opt-in settings (e.g. change-source-branch script)
|
|
66
70
|
│ │ ├── content-migration-service.ts # runtime legacy ws_events → normalised AgentEvent migration
|
|
67
71
|
│ │ ├── templates-service.ts # prompt templates CRUD (JSON file persistence, seeding)
|
|
68
72
|
│ │ ├── dev-server-service.ts # per-workspace dev server lifecycle (docker or npm process)
|
|
@@ -82,8 +86,8 @@ src/
|
|
|
82
86
|
├── client/ # Vue 3 + Quasar SPA
|
|
83
87
|
│ └── src/
|
|
84
88
|
│ ├── stores/ # pinia: workspace, websocket, settings, dev-server, templates
|
|
85
|
-
│ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel…
|
|
86
|
-
│ ├── utils/ # expand-template (
|
|
89
|
+
│ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel, WorkspaceAttentionLabels…
|
|
90
|
+
│ ├── utils/ # expand-template, formatters, workspace-attention (CI failure / changes-requested derivation)…
|
|
87
91
|
│ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
|
|
88
92
|
│ └── router/
|
|
89
93
|
├── mcp-server/ # standalone MCP server spawned per workspace
|
|
@@ -101,6 +105,7 @@ src/
|
|
|
101
105
|
| `agent_sessions` | Claude Code CLI invocations — pid, `claude_session_id`, status, started_at, ended_at, `name` |
|
|
102
106
|
| `ws_events` | persisted WebSocket events for replay on reconnect — type, payload, session_id, created_at |
|
|
103
107
|
| `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool — target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
|
|
108
|
+
| `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 |
|
|
104
109
|
|
|
105
110
|
`status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
|
|
106
111
|
|
|
@@ -157,6 +162,10 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
157
162
|
|
|
158
163
|
## External integrations
|
|
159
164
|
|
|
165
|
+
### Forge providers
|
|
166
|
+
|
|
167
|
+
`src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (no-op — disables PR/MR features cleanly). The public surface is two functions: `getForgeProvider(name)` in `registry.ts` (returns the provider for a named forge) and `resolveForge(projectPath)` in `resolve.ts` (reads the per-project `forge` setting, then falls back to auto-detection from the `origin` remote URL — host contains `github.com` → GitHub, host contains `gitlab` → GitLab, otherwise `none`). The per-project `forge` setting (`'auto' | 'github' | 'gitlab' | 'none'`, default `'auto'`) is stored in `settings.json` and seeded by settings migration v32. PR routes (`open-pr`, `change-pr-base`) and the pr-watcher go through the resolved provider. **Kōbō ships no forge credentials** — the user must install and authenticate `gh` or `glab` themselves; when the CLI is absent or unauthenticated, PR/MR actions are disabled with a tooltip rather than a raw error.
|
|
168
|
+
|
|
160
169
|
### Notion (opt-in, user-provided credentials)
|
|
161
170
|
|
|
162
171
|
`notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials** — the feature only works if the user has configured their own integration token. The token is resolved in this order:
|
|
@@ -175,7 +184,7 @@ See the "Notion integration" section of the README for the end-user setup guide.
|
|
|
175
184
|
|
|
176
185
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
177
186
|
|
|
178
|
-
**Claude Code** (`claude-code/`) — uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var.
|
|
187
|
+
**Claude Code** (`claude-code/`) — uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
|
|
179
188
|
|
|
180
189
|
**OpenAI Codex** (`codex/`) — uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
181
190
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts` — generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -200,6 +209,28 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
200
209
|
- **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period — subsequent scrolls during a burst snap instantly.
|
|
201
210
|
- **`MCP tools` need `default_tools_approval_mode: 'auto'` in `config.mcp_servers`.** Without it Codex flags every MCP tool call as needing user approval ("user cancelled MCP tool call"). Kōbō trusts every tool it spawns, so the options-builder pre-approves the namespace.
|
|
202
211
|
|
|
212
|
+
## Workspace operations
|
|
213
|
+
|
|
214
|
+
### Change source branch
|
|
215
|
+
|
|
216
|
+
`change-source-branch-service.ts` re-targets a workspace onto a new source branch. The default path is a cherry-pick of the branch-proper commits (commits in the working branch but in **neither** the old nor the new base), inspired by the sekur `deploy-preprod-rebase.yml` workflow. The route is `POST /api/workspaces/:id/change-source-branch` and returns a discriminated status: `done | aligned | conflict | too-many | dirty`.
|
|
217
|
+
|
|
218
|
+
- **Built-in cherry-pick** — `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
|
|
219
|
+
- **Custom bash override** — if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty — empty = feature disabled (opt-in).
|
|
220
|
+
- **Custom-script env vars** — `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
|
|
221
|
+
|
|
222
|
+
### Workspace attention indicators
|
|
223
|
+
|
|
224
|
+
`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, no IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
|
|
225
|
+
|
|
226
|
+
### Bulk workspace info refresh
|
|
227
|
+
|
|
228
|
+
`GET /api/workspaces/info` returns `{ workspaces, prSnapshots, gitStats }` in one shot. The client polls this endpoint every 30 s so every non-archived workspace stays ≤ 30 s fresh without a per-card stats request. 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.
|
|
229
|
+
|
|
230
|
+
### File editing in the diff viewer
|
|
231
|
+
|
|
232
|
+
The right panel of `DiffViewer.vue` is editable when the workspace agent is stopped and the file is not in `deleted` status. `Ctrl/Cmd+S` or the explicit Save button persists the file via `POST /api/workspaces/:id/save-file`, sending `{ path, content, baseSha }` where `baseSha` is the sha256 captured at `GET /diff-file` time. The route refuses with **412 Precondition Failed** + `{ currentSha }` if the on-disk content has changed; the client shows a Reload / Keep mine dialog. Worktree-traversal guards (including parent-symlink escapes) and a 1 MB size cap live in `file-editor-service.ts`. Changing files in the tree while dirty pops an "Unsaved changes" prompt.
|
|
233
|
+
|
|
203
234
|
## Code conventions
|
|
204
235
|
|
|
205
236
|
**Service layer** throws descriptive errors; the route layer catches and maps to HTTP status codes. Error messages follow the pattern `` `Workspace '${id}' not found` `` / `` `... is already archived` ``.
|
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
|
+
## 1.7.18
|
|
8
|
+
|
|
9
|
+
- chore(audit): fix npm audit
|
|
10
|
+
- feat(client): collapsible ask-user-question panel
|
|
11
|
+
- feat: per-workspace chat history + inline file editing in the diff viewer
|
|
12
|
+
- feat: multi-forge, change source branch, workspace attention
|
|
13
|
+
|
|
14
|
+
## 1.7.17
|
|
15
|
+
|
|
16
|
+
- feat: per-workspace chat history + inline file editing in the diff viewer
|
|
17
|
+
- feat: multi-forge, change source branch, pr-watcher
|
|
18
|
+
|
|
7
19
|
## 1.7.16
|
|
8
20
|
|
|
9
21
|
- feat(engine): handle user interruptions as clean stops
|
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
17
17
|
- **Two agent engines** — Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
|
|
18
18
|
- **Live chat** — streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback; `/` autocompletes skills & commands and `@` fuzzy-autocompletes worktree file paths; every workspace's session events are exportable to CSV.
|
|
19
19
|
- **Task tracking** — per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
|
|
20
|
-
- **Git panel** — Monaco-based diff viewer, inline conflict resolution, `Sync` / `Push` / `Open PR`
|
|
20
|
+
- **Git panel** — Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch` (cherry-pick of the branch-proper commits, with an optional custom bash script). Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge — auto-detected from the remote, overridable per project.
|
|
21
|
+
- **Attention indicators** — workspace cards in the drawer surface CI failures and review-requested-changes inline, so failing PRs/MRs stand out at a glance.
|
|
21
22
|
- **Auto-loop** — opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
|
|
22
23
|
- **Quota-aware** — 5-hour / 7-day Claude usage and Codex rate-limit buckets in the footer; sessions auto-resume after a rate-limit reset.
|
|
23
24
|
- **Scheduled wakeups** — the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool; Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
|
|
@@ -235,6 +235,44 @@ export function listWorkspaceImagesHandler(worktreePath) {
|
|
|
235
235
|
};
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* `## Source` block markers written by the workspace creation route — a
|
|
240
|
+
* `- Notion: <url>` line for Notion imports, `- Sentry: <url>` for Sentry.
|
|
241
|
+
* The marker is the discriminator: it is what tells a ticket file apart from
|
|
242
|
+
* an agent note, and Notion apart from Sentry.
|
|
243
|
+
*/
|
|
244
|
+
const SOURCE_MARKER = /^- (Notion|Sentry): (.+)$/m;
|
|
245
|
+
/**
|
|
246
|
+
* Read the mission's source-of-truth ticket(s). Both the Notion and Sentry
|
|
247
|
+
* importers write their extracted brief into `.ai/thoughts/` at workspace
|
|
248
|
+
* creation; this returns one entry per ticket file, typed by origin.
|
|
249
|
+
*
|
|
250
|
+
* Only `.md` files at the *root* of `.ai/thoughts/` are considered — agent
|
|
251
|
+
* notes live under `.ai/thoughts/logs/` and are deliberately skipped. Files
|
|
252
|
+
* with no recognised `## Source` marker (stray notes) are skipped too.
|
|
253
|
+
* Sorted by `type` for a deterministic order. Empty array when nothing matches.
|
|
254
|
+
*/
|
|
255
|
+
export function getTicketSourcesHandler(worktreePath) {
|
|
256
|
+
const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts');
|
|
257
|
+
if (!fs.existsSync(thoughtsDir))
|
|
258
|
+
return [];
|
|
259
|
+
const sources = [];
|
|
260
|
+
for (const entry of fs.readdirSync(thoughtsDir, { withFileTypes: true })) {
|
|
261
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
262
|
+
continue;
|
|
263
|
+
const content = fs.readFileSync(path.join(thoughtsDir, entry.name), 'utf-8');
|
|
264
|
+
const match = content.match(SOURCE_MARKER);
|
|
265
|
+
if (!match)
|
|
266
|
+
continue;
|
|
267
|
+
sources.push({
|
|
268
|
+
type: match[1].toLowerCase(),
|
|
269
|
+
url: match[2].trim() || null,
|
|
270
|
+
content: content.trim(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
sources.sort((a, b) => a.type.localeCompare(b.type));
|
|
274
|
+
return sources;
|
|
275
|
+
}
|
|
238
276
|
// ── Documents ────────────────────────────────────────────────────────────────
|
|
239
277
|
/** Directories (relative to the worktree root) scanned for AI-generated docs. */
|
|
240
278
|
export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
|
|
@@ -308,9 +346,14 @@ export function readDocumentHandler(worktreePath, relPath) {
|
|
|
308
346
|
return { path: normalized, content: fs.readFileSync(abs, 'utf-8') };
|
|
309
347
|
}
|
|
310
348
|
/**
|
|
311
|
-
* Append a thought / decision / note to
|
|
312
|
-
* Creates the directory if missing.
|
|
313
|
-
* the file actually written — useful
|
|
349
|
+
* Append a thought / decision / note to
|
|
350
|
+
* `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>.md`. Creates the directory if missing.
|
|
351
|
+
* Returns the path (worktree-relative) of the file actually written — useful
|
|
352
|
+
* for the agent to reference it in chat.
|
|
353
|
+
*
|
|
354
|
+
* Notes live in the `logs/` sub-directory so they stay separate from the
|
|
355
|
+
* mission's source-of-truth ticket files at the root of `.ai/thoughts/` —
|
|
356
|
+
* `get_ticket` reads only the root and therefore never picks up agent notes.
|
|
314
357
|
*/
|
|
315
358
|
export function logThoughtHandler(worktreePath, data) {
|
|
316
359
|
const title = data.title?.trim();
|
|
@@ -319,7 +362,7 @@ export function logThoughtHandler(worktreePath, data) {
|
|
|
319
362
|
const content = data.content?.trim();
|
|
320
363
|
if (!content)
|
|
321
364
|
throw new Error('content is required');
|
|
322
|
-
const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts');
|
|
365
|
+
const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts', 'logs');
|
|
323
366
|
fs.mkdirSync(thoughtsDir, { recursive: true });
|
|
324
367
|
const date = new Date().toISOString().slice(0, 10);
|
|
325
368
|
const slug = title
|
|
@@ -332,7 +375,7 @@ export function logThoughtHandler(worktreePath, data) {
|
|
|
332
375
|
const tagSuffix = data.tag ? `-${data.tag.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}` : '';
|
|
333
376
|
const filename = `${date}-${slug}${tagSuffix}.md`;
|
|
334
377
|
const abs = path.join(thoughtsDir, filename);
|
|
335
|
-
const relPath = `.ai/thoughts/${filename}`;
|
|
378
|
+
const relPath = `.ai/thoughts/logs/${filename}`;
|
|
336
379
|
const header = `# ${title}\n\n_${new Date().toISOString()}_${data.tag ? ` · tag: \`${data.tag}\`` : ''}\n\n`;
|
|
337
380
|
fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), 'utf-8');
|
|
338
381
|
return { path: relPath };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
5
|
import { getDb } from '../server/db/index.js';
|
|
8
6
|
import { runMigrations } from '../server/db/migrations.js';
|
|
9
|
-
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
7
|
+
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
10
8
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
11
9
|
const dbPath = process.env.KOBO_DB_PATH;
|
|
12
10
|
const settingsPath = process.env.KOBO_SETTINGS_PATH;
|
|
@@ -283,8 +281,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
283
281
|
annotations: { destructiveHint: false, openWorldHint: false },
|
|
284
282
|
},
|
|
285
283
|
{
|
|
286
|
-
name: '
|
|
287
|
-
description: 'CALL when the user references "the ticket", "the Notion page", or when you need the source-of-truth text for the mission.
|
|
284
|
+
name: 'get_ticket',
|
|
285
|
+
description: 'CALL when the user references "the ticket", "the issue", "the Notion page", or when you need the source-of-truth text for the mission. Works for any source — a Notion ticket or a Sentry issue. Returns `{ sources: [{ type, url, content }] }`, one entry per imported ticket (type is "notion" or "sentry"). Usually one source; empty when none was imported.',
|
|
288
286
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
289
287
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
290
288
|
},
|
|
@@ -363,7 +361,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
363
361
|
},
|
|
364
362
|
{
|
|
365
363
|
name: 'log_thought',
|
|
366
|
-
description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
|
|
364
|
+
description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/logs/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
|
|
367
365
|
inputSchema: {
|
|
368
366
|
type: 'object',
|
|
369
367
|
properties: {
|
|
@@ -589,21 +587,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
589
587
|
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
590
588
|
return ok(listWorkspaceImagesHandler(info.worktreePath));
|
|
591
589
|
}
|
|
592
|
-
|
|
590
|
+
// `get_notion_ticket` is the pre-1.7 name, kept as a back-compat alias so
|
|
591
|
+
// sessions resumed against an older tool list still resolve.
|
|
592
|
+
if (name === 'get_ticket' || name === 'get_notion_ticket') {
|
|
593
593
|
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
594
|
-
|
|
595
|
-
let ticketContent = '';
|
|
596
|
-
if (fs.existsSync(thoughtsDir)) {
|
|
597
|
-
const files = fs.readdirSync(thoughtsDir).filter((f) => f.endsWith('.md'));
|
|
598
|
-
for (const file of files) {
|
|
599
|
-
ticketContent += `${fs.readFileSync(path.join(thoughtsDir, file), 'utf-8')}\n`;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return ok({
|
|
603
|
-
notionUrl: info.notionUrl,
|
|
604
|
-
notionPageId: info.notionPageId,
|
|
605
|
-
ticketContent: ticketContent.trim() || null,
|
|
606
|
-
});
|
|
594
|
+
return ok({ sources: getTicketSourcesHandler(info.worktreePath) });
|
|
607
595
|
}
|
|
608
596
|
if (name === 'get_git_info') {
|
|
609
597
|
const result = await backendRequest('GET', `/api/workspaces/${workspaceId}/git-stats`);
|
|
@@ -281,6 +281,25 @@ export const migrations = [
|
|
|
281
281
|
}
|
|
282
282
|
},
|
|
283
283
|
},
|
|
284
|
+
{
|
|
285
|
+
version: 24,
|
|
286
|
+
name: 'add-workspace-chat-history-table',
|
|
287
|
+
migrate: (db) => {
|
|
288
|
+
// Per-workspace chat history (user-typed messages). CASCADE on workspace
|
|
289
|
+
// delete. Index supports the typical "latest N for a workspace" query via
|
|
290
|
+
// (workspace_id, id DESC).
|
|
291
|
+
db.exec(`
|
|
292
|
+
CREATE TABLE IF NOT EXISTS workspace_chat_history (
|
|
293
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
294
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
295
|
+
message TEXT NOT NULL,
|
|
296
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
297
|
+
);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS idx_workspace_chat_history_workspace_id_id
|
|
299
|
+
ON workspace_chat_history(workspace_id, id DESC);
|
|
300
|
+
`);
|
|
301
|
+
},
|
|
302
|
+
},
|
|
284
303
|
];
|
|
285
304
|
/** Current schema version — always equals the highest migration version. */
|
|
286
305
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -106,8 +106,17 @@ export function initSchema(db) {
|
|
|
106
106
|
fetched_at TEXT NOT NULL
|
|
107
107
|
);
|
|
108
108
|
|
|
109
|
+
CREATE TABLE IF NOT EXISTS workspace_chat_history (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
112
|
+
message TEXT NOT NULL,
|
|
113
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
114
|
+
);
|
|
115
|
+
|
|
109
116
|
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id);
|
|
110
117
|
CREATE INDEX IF NOT EXISTS idx_agent_sessions_workspace_id ON agent_sessions(workspace_id);
|
|
111
118
|
CREATE INDEX IF NOT EXISTS idx_ws_events_workspace_id ON ws_events(workspace_id);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_workspace_chat_history_workspace_id_id
|
|
120
|
+
ON workspace_chat_history(workspace_id, id DESC);
|
|
112
121
|
`);
|
|
113
122
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
3
3
|
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from '../services/review-template-service.js';
|
|
4
|
+
import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaults.js';
|
|
4
5
|
import * as settingsService from '../services/settings-service.js';
|
|
5
6
|
import { DEFAULT_GIT_CONVENTIONS, DEFAULT_PR_PROMPT_TEMPLATE, } from '../services/settings-service.js';
|
|
6
7
|
import { listTemplates, replaceAllTemplates } from '../services/templates-service.js';
|
|
@@ -39,6 +40,7 @@ app.get('/defaults', (c) => {
|
|
|
39
40
|
gitConventions: DEFAULT_GIT_CONVENTIONS,
|
|
40
41
|
notionInitialPromptTemplate: DEFAULT_NOTION_INITIAL_PROMPT,
|
|
41
42
|
sentryInitialPromptTemplate: DEFAULT_SENTRY_INITIAL_PROMPT,
|
|
43
|
+
changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
46
|
// GET /api/settings/mcp-servers — list active MCP servers from Claude config
|