@loicngr/kobo 1.6.12 → 1.6.13
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/README.md +10 -6
- package/dist/mcp-server/kobo-tasks-handlers.js +2 -2
- package/dist/server/db/migrations.js +24 -0
- package/dist/server/db/schema.js +10 -0
- package/dist/server/index.js +27 -4
- package/dist/server/routes/documents.js +2 -2
- package/dist/server/routes/git.js +21 -0
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/images.js +3 -3
- package/dist/server/routes/usage.js +18 -0
- package/dist/server/routes/workspaces.js +207 -81
- package/dist/server/services/agent/engines/claude-code/args-builder.js +2 -0
- package/dist/server/services/agent/orchestrator.js +1 -1
- package/dist/server/services/auto-loop-service.js +22 -4
- package/dist/server/services/dev-server-service.js +2 -5
- package/dist/server/services/settings-service.js +18 -2
- package/dist/server/services/usage/db.js +29 -0
- package/dist/server/services/usage/index.js +2 -0
- package/dist/server/services/usage/poller.js +52 -0
- package/dist/server/services/usage/providers/claude-code.js +93 -0
- package/dist/server/services/usage/types.js +1 -0
- package/dist/server/services/wakeup-service.js +2 -2
- package/dist/server/services/websocket-service.js +14 -0
- package/dist/server/services/workspace-service.js +28 -3
- package/dist/server/services/worktree-service.js +50 -0
- package/dist/server/utils/mcp-client.js +7 -0
- package/dist/shared/auto-loop-prompts.js +46 -8
- package/package.json +1 -1
- package/src/client/dist/spa/assets/{ActivityFeed-6Xg7qNfy.js → ActivityFeed-BsY3-q5d.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-Cdhkkx-X.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-PRvhol1N.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-T111s7BH.js → DiffViewer-DXcoEtVq.js} +2 -2
- package/src/client/dist/spa/assets/{HealthPage-1VakQ0x_.js → HealthPage-BSyGqDRu.js} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-Ci-CETJi.css → MainLayout-D2SfvksB.css} +1 -1
- package/src/client/dist/spa/assets/{MainLayout-w7DoW3yz.js → MainLayout-EYaLqjJx.js} +17 -17
- package/src/client/dist/spa/assets/{SearchPage-CcldJX8i.js → SearchPage-Bgx02GOH.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-BTSOovDV.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-CwLELxfl.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-C5MZx1sZ.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-C8dJWu-n.js +4 -0
- package/src/client/dist/spa/assets/{build-path-tree-DbuI5yRz.js → build-path-tree-D-2LpB2J.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-DhpmJAZc.js → cssMode-DVBmJp-B.js} +1 -1
- package/src/client/dist/spa/assets/{documents-fVD9RJth.js → documents-Ck8VwvpQ.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-DCvwHsju.js → editor.api-DgbPJaK4.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-CRtPC0iL.js → editor.main-BqqoRfAU.js} +3 -3
- package/src/client/dist/spa/assets/{expand-template-BIra7NIw.js → expand-template-bkCTc78P.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-C9UOErQw.js → freemarker2-CgaW0Q0y.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-DmZ2-ZcJ.js → handlebars-BSs5PdXe.js} +1 -1
- package/src/client/dist/spa/assets/{html-ButyxlXG.js → html-C9wlJaMs.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-C-defy1b.js → htmlMode-DaRssGJk.js} +1 -1
- package/src/client/dist/spa/assets/i18n-BSNIShFg.js +1 -0
- package/src/client/dist/spa/assets/index-odgA9x8A.js +2 -0
- package/src/client/dist/spa/assets/{javascript-B6zVweIF.js → javascript-D0VYhsc-.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-CttMw-EY.js → jsonMode-B57EaUNS.js} +1 -1
- package/src/client/dist/spa/assets/kobo-commands-D-9dbM70.js +11 -0
- package/src/client/dist/spa/assets/{liquid-tGpdE1YW.js → liquid-gP2gg7sw.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-Cy5mpQoy.js → mdx-HhXcZn_S.js} +1 -1
- package/src/client/dist/spa/assets/{models-DdAQDnqk.js → models-CJC61gWE.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DtdkkTgR.js → monaco.contribution-ChJg8bwd.js} +2 -2
- package/src/client/dist/spa/assets/{python-hLOxMbm9.js → python-DM6FfMV3.js} +1 -1
- package/src/client/dist/spa/assets/{razor-tqHFRROa.js → razor-XifsxhTG.js} +1 -1
- package/src/client/dist/spa/assets/stats-C3n1k51k.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-MJKgZYsJ.js → tsMode-B8gurPqG.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-CWTqB5lb.js → typescript-CZKTCOjl.js} +1 -1
- package/src/client/dist/spa/assets/{xml-ByDBLBVa.js → xml-CtZPkb7Q.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-BiTCWZ38.js → yaml-D5IEE5M-.js} +1 -1
- package/src/client/dist/spa/index.html +1 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +3 -2
- package/src/client/dist/spa/assets/CreatePage-BQu7mQjm.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-CrRGDs5V.js +0 -2
- package/src/client/dist/spa/assets/SettingsPage-CLMCHMpz.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CMyeaz63.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-Bw9xhTDR.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-_1mty_a4.css +0 -1
- package/src/client/dist/spa/assets/i18n-C8aJvuyS.js +0 -1
- package/src/client/dist/spa/assets/index-DAbX631s.js +0 -2
- package/src/client/dist/spa/assets/kobo-commands-CD7ERFxp.js +0 -10
- package/src/client/dist/spa/assets/rate-limit-labels-BaD9dQtl.js +0 -1
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
|
|
|
17
17
|
- **Task & acceptance criteria tracking** — the agent reports progress through a dedicated MCP server (`kobo-tasks`) that reads and updates tasks directly from the SQLite database
|
|
18
18
|
- **Documents panel** — tree view in the right drawer that surfaces every AI-generated markdown file under `docs/plans/`, `docs/superpowers/`, and `.ai/thoughts/`. Paths mentioned in chat messages are auto-detected against the catalogue and become one-click deep-links into the panel
|
|
19
19
|
- **Git panel with inline diff viewer** — Monaco-powered side-by-side / inline diff of the working branch against its source, with file tree (same q-tree as Documents), inline rebase/merge conflict resolution, and a clean action bar: `Sync` split-button (pull / rebase / merge), `Push`, `Diff`, `Create PR`
|
|
20
|
-
- **Notion integration** — pull workspace missions straight from Notion pages, extract markdown, and use it as the source of truth for acceptance criteria
|
|
21
|
-
- **Sentry integration** — paste a Sentry issue URL to spin up a dedicated "fix workspace" with the stacktrace, tags, and offending spans written to `.ai/thoughts/SENTRY-<id>.md`; the agent is primed with a TDD fix workflow and has access to the Sentry MCP tools for deeper digging
|
|
20
|
+
- **Notion integration** — pull workspace missions straight from Notion pages, extract markdown, and use it as the source of truth for acceptance criteria. Right-click a Notion-sourced workspace to jump back to its source page in one click
|
|
21
|
+
- **Sentry integration** — paste a Sentry issue URL to spin up a dedicated "fix workspace" with the stacktrace, tags, and offending spans written to `.ai/thoughts/SENTRY-<id>.md`; the agent is primed with a TDD fix workflow and has access to the Sentry MCP tools for deeper digging. Right-click reopens the Sentry issue in Sentry's UI
|
|
22
22
|
- **Per-workspace dev servers** — start/stop Docker or Node dev servers scoped to each branch, with log streaming
|
|
23
23
|
- **Conventional-commit enforcement** — project-level git conventions are written to `.ai/.git-conventions.md` inside every workspace so Claude follows them during commits
|
|
24
24
|
- **Pull request automation** — one-click `push`, `pull`, `open-pr`, and "change PR base" endpoints integrate with the GitHub CLI, using a configurable prompt template
|
|
@@ -26,11 +26,12 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
|
|
|
26
26
|
- **Prompt templates** — personal library of reusable prompts with variable substitution (`{working_branch}`, `{commit_count}`, etc.), insertable from the chat input via `/` autocomplete; editable in Settings > Templates
|
|
27
27
|
- **Favorites and tags** — pin workspaces to the top via right-click favourite, organise with per-workspace tags filterable from the sidebar; a global tag catalogue keeps colours consistent across workspaces
|
|
28
28
|
- **Health panel + config export/import** — inspect backend health (agent sessions, migration state, dev servers, DB size) and roundtrip your Kōbō config (settings, templates, skills) between machines via JSON
|
|
29
|
-
- **
|
|
29
|
+
- **Account-level quota panel** — a colored mini-bar badge in the chat footer shows the current Claude Code 5-hour and 7-day usage, fed by a backend service that polls Anthropic's OAuth usage endpoint every 60 seconds. Click to open a popover with full bars, reset times, a "Refresh now" button, and a one-click jump to the Stats tab. Pluggable per-provider (Codex-ready), persisted in SQLite so the badge is populated on cold start, and account-level so it's the same across workspaces sharing the same engine
|
|
30
30
|
- **Resizable right drawer** — drag-to-resize horizontally and vertically, with tab state and split ratio persisted to localStorage
|
|
31
31
|
- **Soft interrupt** — pause an agent mid-execution (SIGINT, like pressing Escape in Claude Code) without killing the process; the agent stops the current tool and waits for the next message
|
|
32
32
|
- **Archive instead of delete** — soft-remove workspaces without losing the worktree, branches, or history; unarchive restores the exact pre-archive state
|
|
33
|
-
- **Auto-loop mode** — opt-in, per-workspace: when enabled, Kōbō spawns a fresh Claude session for the next pending task after every `session:ended`, walking through the task list until all are `done`. Stops automatically on error, on stall (3 consecutive sessions with no task completed), or when the user clicks Stop. A grooming step (`/kobo-prep-autoloop`) ensures tasks are atomic before the loop runs; Notion-imported workspaces with both todos and acceptance criteria are auto-unlocked
|
|
33
|
+
- **Auto-loop mode** — opt-in, per-workspace: when enabled, Kōbō spawns a fresh Claude session for the next pending task after every `session:ended`, walking through the task list until all are `done`. Stops automatically on error, on stall (3 consecutive sessions with no task completed), or when the user clicks Stop. A grooming step (`/kobo-prep-autoloop`) ensures tasks are atomic before the loop runs; Notion-imported workspaces with both todos and acceptance criteria are auto-unlocked. **E2E grooming** — when a project declares an E2E framework in Settings (Cypress, Playwright, Vitest, etc.), the grooming phase injects an `[E2E] ` test sub-task between every parent task; each iteration then runs the matching E2E suite as part of its acceptance check
|
|
34
|
+
- **Attach existing worktrees** — Kōbō detects orphan worktrees under `.worktrees/` (created outside Kōbō, or left over from an earlier install) and lets you attach them to a new workspace from the creation form, picking up the existing branch and folder instead of cloning a new one
|
|
34
35
|
- **Quota-aware retry backoff** — when a Claude rate limit is hit mid-session, Kōbō schedules the retry at the actual reset time reported by the API (via `rate_limit.info.buckets[].resetsAt`), falling back to a 15 → 30 → 60 → 180 → 300 min ladder only when the reset info is missing or implausible
|
|
35
36
|
- **Scheduled wakeups** — the `ScheduleWakeup` tool is honoured server-side: Kōbō persists the wakeup in SQLite, rehydrates on restart, and respawns the agent with `--resume` at the target time
|
|
36
37
|
|
|
@@ -99,7 +100,7 @@ npm start # runs the compiled server
|
|
|
99
100
|
### Test & lint
|
|
100
101
|
|
|
101
102
|
```bash
|
|
102
|
-
npm test # backend vitest suite (
|
|
103
|
+
npm test # backend vitest suite (950+ tests)
|
|
103
104
|
npm run test:client # client vitest suite (Pinia stores + pure utils, 85+ tests)
|
|
104
105
|
npm run test:all # backend + client suites
|
|
105
106
|
npm run lint # biome check (lint + format verification)
|
|
@@ -226,8 +227,9 @@ src/
|
|
|
226
227
|
│ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
|
|
227
228
|
│ │ │ └── engines/claude-code/ # spawn + NDJSON stream-parser + args-builder + mcp-config + capabilities
|
|
228
229
|
│ │ ├── content-migration-service.ts # legacy ws_events → normalised AgentEvent rows, with DB backup
|
|
230
|
+
│ │ ├── usage/ # pluggable quota provider, 60s poller, persistence, WS broadcast
|
|
229
231
|
│ │ └── … # workspace, dev-server, ws, notion, sentry, settings, pr-template
|
|
230
|
-
│ ├── routes/ # Hono handlers (workspaces, engines, migration, templates, …)
|
|
232
|
+
│ ├── routes/ # Hono handlers (workspaces, engines, migration, templates, usage, …)
|
|
231
233
|
│ └── utils/ # git-ops, process-tracker, paths
|
|
232
234
|
├── shared/ # modules shared by backend and frontend (e.g. model catalogue)
|
|
233
235
|
├── client/ # Vue 3 + Quasar SPA
|
|
@@ -253,6 +255,7 @@ See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, W
|
|
|
253
255
|
| `tasks` | workspace sub-items — tasks and acceptance criteria |
|
|
254
256
|
| `agent_sessions` | agent runs — pid, `engine_session_id`, lifecycle |
|
|
255
257
|
| `ws_events` | persisted WebSocket events (chat history, `agent:event` stream, user messages) for replay on reconnect |
|
|
258
|
+
| `usage_snapshots` | latest quota snapshot per provider (one row per `provider_id`) — populated by the 60s polling loop, used for cold-start hydration of the chat-footer quota badge |
|
|
256
259
|
|
|
257
260
|
## MCP server
|
|
258
261
|
|
|
@@ -271,6 +274,7 @@ Kōbō reads settings from `~/.config/kobo/settings.json` (or falls back to defa
|
|
|
271
274
|
- `prPromptTemplate` — template rendered when opening a PR via the `/open-pr` endpoint; supports `{{pr_number}}`, `{{pr_url}}`, `{{branch_name}}`, `{{diff_stats}}`, `{{commits}}`, etc.
|
|
272
275
|
- `gitConventions` — markdown-formatted git conventions written to `.ai/.git-conventions.md` in every workspace so the agent follows them when committing
|
|
273
276
|
- `devServer` — per-project `startCommand` / `stopCommand` for launching workspace-scoped dev servers
|
|
277
|
+
- `e2e` — per-project E2E test framework (`cypress`, `playwright`, `jest`, `vitest`, `other`, or none) plus an optional skill name and prompt; consumed by the auto-loop grooming step to inject `[E2E] ` test sub-tasks alongside parent tasks
|
|
274
278
|
|
|
275
279
|
## Contributing
|
|
276
280
|
|
|
@@ -152,7 +152,7 @@ export function getSettingsHandler(settingsPath, projectPath) {
|
|
|
152
152
|
/** Fetch workspace metadata from the database, computing the worktree path from project_path and working_branch. */
|
|
153
153
|
export function getWorkspaceInfoHandler(db, workspaceId) {
|
|
154
154
|
const row = db
|
|
155
|
-
.prepare('SELECT id, name, project_path, source_branch, working_branch, status, notion_url, notion_page_id, model, dev_server_status, has_unread, auto_loop, auto_loop_ready, created_at, updated_at FROM workspaces WHERE id = ?')
|
|
155
|
+
.prepare('SELECT id, name, project_path, source_branch, working_branch, worktree_path, status, notion_url, notion_page_id, model, dev_server_status, has_unread, auto_loop, auto_loop_ready, created_at, updated_at FROM workspaces WHERE id = ?')
|
|
156
156
|
.get(workspaceId);
|
|
157
157
|
if (!row) {
|
|
158
158
|
throw new Error(`Workspace '${workspaceId}' not found`);
|
|
@@ -163,7 +163,7 @@ export function getWorkspaceInfoHandler(db, workspaceId) {
|
|
|
163
163
|
projectPath: row.project_path,
|
|
164
164
|
sourceBranch: row.source_branch,
|
|
165
165
|
workingBranch: row.working_branch,
|
|
166
|
-
worktreePath: path.join(row.project_path, '.worktrees', row.working_branch),
|
|
166
|
+
worktreePath: row.worktree_path ?? path.join(row.project_path, '.worktrees', row.working_branch),
|
|
167
167
|
status: row.status,
|
|
168
168
|
model: row.model,
|
|
169
169
|
notionUrl: row.notion_url,
|
|
@@ -130,6 +130,30 @@ export const migrations = [
|
|
|
130
130
|
db.prepare('ALTER TABLE workspaces ADD COLUMN sentry_url TEXT').run();
|
|
131
131
|
},
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
version: 15,
|
|
135
|
+
name: 'add-workspace-worktree-path',
|
|
136
|
+
migrate: (db) => {
|
|
137
|
+
db.transaction(() => {
|
|
138
|
+
db.prepare('ALTER TABLE workspaces ADD COLUMN worktree_path TEXT').run();
|
|
139
|
+
db.prepare('ALTER TABLE workspaces ADD COLUMN worktree_owned INTEGER NOT NULL DEFAULT 1').run();
|
|
140
|
+
db.prepare("UPDATE workspaces SET worktree_path = project_path || '/.worktrees/' || working_branch WHERE worktree_path IS NULL").run();
|
|
141
|
+
})();
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
version: 16,
|
|
146
|
+
name: 'add-usage-snapshots-table',
|
|
147
|
+
migrate: (db) => {
|
|
148
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
149
|
+
provider_id TEXT PRIMARY KEY,
|
|
150
|
+
status TEXT NOT NULL,
|
|
151
|
+
error_message TEXT,
|
|
152
|
+
buckets_json TEXT NOT NULL,
|
|
153
|
+
fetched_at TEXT NOT NULL
|
|
154
|
+
)`).run();
|
|
155
|
+
},
|
|
156
|
+
},
|
|
133
157
|
];
|
|
134
158
|
/** Current schema version — always equals the highest migration version. */
|
|
135
159
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -11,6 +11,8 @@ export function initSchema(db) {
|
|
|
11
11
|
notion_url TEXT,
|
|
12
12
|
notion_page_id TEXT,
|
|
13
13
|
sentry_url TEXT,
|
|
14
|
+
worktree_path TEXT,
|
|
15
|
+
worktree_owned INTEGER NOT NULL DEFAULT 1,
|
|
14
16
|
model TEXT NOT NULL DEFAULT 'claude-opus-4-7',
|
|
15
17
|
reasoning_effort TEXT NOT NULL DEFAULT 'auto',
|
|
16
18
|
permission_mode TEXT NOT NULL DEFAULT 'auto-accept',
|
|
@@ -67,6 +69,14 @@ export function initSchema(db) {
|
|
|
67
69
|
created_at TEXT NOT NULL
|
|
68
70
|
);
|
|
69
71
|
|
|
72
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
73
|
+
provider_id TEXT PRIMARY KEY,
|
|
74
|
+
status TEXT NOT NULL,
|
|
75
|
+
error_message TEXT,
|
|
76
|
+
buckets_json TEXT NOT NULL,
|
|
77
|
+
fetched_at TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
|
|
70
80
|
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id);
|
|
71
81
|
CREATE INDEX IF NOT EXISTS idx_agent_sessions_workspace_id ON agent_sessions(workspace_id);
|
|
72
82
|
CREATE INDEX IF NOT EXISTS idx_ws_events_workspace_id ON ws_events(workspace_id);
|
package/dist/server/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import searchRouter from './routes/search.js';
|
|
|
19
19
|
import sentryRouter from './routes/sentry.js';
|
|
20
20
|
import settingsRouter from './routes/settings.js';
|
|
21
21
|
import templatesRouter from './routes/templates.js';
|
|
22
|
+
import usageRoutes from './routes/usage.js';
|
|
22
23
|
import workspacesRouter from './routes/workspaces.js';
|
|
23
24
|
import { getAvailableSkills, reconcileOrphanSessions, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
24
25
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
@@ -27,8 +28,9 @@ import { createDailyDbBackupIfNeeded } from './services/db-backup-service.js';
|
|
|
27
28
|
import { startDevServer, stopDevServer } from './services/dev-server-service.js';
|
|
28
29
|
import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
|
|
29
30
|
import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
|
|
31
|
+
import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
30
32
|
import * as wakeupService from './services/wakeup-service.js';
|
|
31
|
-
import { emit, handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
33
|
+
import { emit, emitEphemeral, handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
32
34
|
import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
|
|
33
35
|
import { getClientSpaPath, getDbPath, getKoboHome, getPackageVersion } from './utils/paths.js';
|
|
34
36
|
import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/process-tracker.js';
|
|
@@ -62,6 +64,7 @@ startWatchdog();
|
|
|
62
64
|
wakeupService.rehydrate();
|
|
63
65
|
autoLoopService.rehydrate();
|
|
64
66
|
startPrWatcher();
|
|
67
|
+
startUsagePoller();
|
|
65
68
|
// Create Hono app
|
|
66
69
|
const app = new Hono();
|
|
67
70
|
// Health check (root / is handled by the SPA catch-all below)
|
|
@@ -75,6 +78,7 @@ app.route('/api/git', gitRouter);
|
|
|
75
78
|
app.route('/api/settings', settingsRouter);
|
|
76
79
|
app.route('/api/dev-server', devServerRouter);
|
|
77
80
|
app.route('/api/templates', templatesRouter);
|
|
81
|
+
app.route('/api/usage', usageRoutes);
|
|
78
82
|
app.route('/api/workspaces', documentsRouter);
|
|
79
83
|
app.route('/api/search', searchRouter);
|
|
80
84
|
app.route('/api/health', healthRouter);
|
|
@@ -200,7 +204,7 @@ terminalWss.on('connection', (ws, workspaceId) => {
|
|
|
200
204
|
ws.send(JSON.stringify({ type: 'error', message: 'Workspace is archived' }));
|
|
201
205
|
return;
|
|
202
206
|
}
|
|
203
|
-
const cwd =
|
|
207
|
+
const cwd = workspace.worktreePath;
|
|
204
208
|
try {
|
|
205
209
|
currentPty = createTerminal(workspaceId, cwd);
|
|
206
210
|
}
|
|
@@ -233,6 +237,19 @@ terminalWss.on('connection', (ws, workspaceId) => {
|
|
|
233
237
|
setMessageHandler((type, payload) => {
|
|
234
238
|
const p = payload;
|
|
235
239
|
if (type === 'chat:message' && p?.workspaceId && p?.content) {
|
|
240
|
+
// Auto-loop owns the agent's turns — a stray user message would land in
|
|
241
|
+
// the middle of an iteration (or in a freshly spawned next one) and break
|
|
242
|
+
// the deterministic loop contract. Reject server-side so direct WS clients
|
|
243
|
+
// can't bypass the frontend's input lock. Grooming phase (ready=0) is
|
|
244
|
+
// skipped — the user must stay free to answer the agent's questions.
|
|
245
|
+
const autoLoopStatus = autoLoopService.getStatus(p.workspaceId);
|
|
246
|
+
if (autoLoopStatus.auto_loop && autoLoopStatus.auto_loop_ready) {
|
|
247
|
+
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
248
|
+
reason: 'auto-loop-active',
|
|
249
|
+
message: 'Auto-loop is running — disable it before sending a message',
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
236
253
|
// Prefer the session explicitly selected by the client (sessionId hint),
|
|
237
254
|
// falling back to the running/most-recent non-idle session so idle sessions
|
|
238
255
|
// never steal the tagging.
|
|
@@ -257,7 +274,7 @@ setMessageHandler((type, payload) => {
|
|
|
257
274
|
try {
|
|
258
275
|
const workspace = getWorkspace(p.workspaceId);
|
|
259
276
|
if (workspace) {
|
|
260
|
-
const worktreePath =
|
|
277
|
+
const worktreePath = workspace.worktreePath;
|
|
261
278
|
// Plan mode blocks MCP tools — when the caller knows the message
|
|
262
279
|
// requires them (e.g. grooming), it sets the override to bypass the
|
|
263
280
|
// workspace default for this spawn only.
|
|
@@ -278,7 +295,7 @@ setMessageHandler((type, payload) => {
|
|
|
278
295
|
console.error(`[ws] workspace:start — workspace '${p.workspaceId}' not found`);
|
|
279
296
|
return;
|
|
280
297
|
}
|
|
281
|
-
const worktreePath =
|
|
298
|
+
const worktreePath = workspace.worktreePath;
|
|
282
299
|
const prompt = p.prompt ?? 'Continue the previous task where you left off.';
|
|
283
300
|
startAgent(p.workspaceId, worktreePath, prompt, workspace.model, false, workspace.permissionMode, undefined, workspace.reasoningEffort);
|
|
284
301
|
}
|
|
@@ -368,6 +385,12 @@ function gracefulShutdown(signal) {
|
|
|
368
385
|
catch {
|
|
369
386
|
// Best-effort
|
|
370
387
|
}
|
|
388
|
+
try {
|
|
389
|
+
stopUsagePoller();
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Best-effort
|
|
393
|
+
}
|
|
371
394
|
// Kill all tracked child processes (agents, dev servers)
|
|
372
395
|
try {
|
|
373
396
|
killAllTrackedProcesses();
|
|
@@ -59,7 +59,7 @@ app.get('/:id/documents', (c) => {
|
|
|
59
59
|
if (!workspace) {
|
|
60
60
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
61
61
|
}
|
|
62
|
-
const worktreePath =
|
|
62
|
+
const worktreePath = workspace.worktreePath;
|
|
63
63
|
const documents = [];
|
|
64
64
|
for (const dir of DOCUMENT_DIRS) {
|
|
65
65
|
const absDir = path.join(worktreePath, dir);
|
|
@@ -97,7 +97,7 @@ app.get('/:id/document', (c) => {
|
|
|
97
97
|
if (!normalized.endsWith(MD_EXT)) {
|
|
98
98
|
return c.json({ error: 'Only .md files can be read' }, 400);
|
|
99
99
|
}
|
|
100
|
-
const worktreePath =
|
|
100
|
+
const worktreePath = workspace.worktreePath;
|
|
101
101
|
const absPath = path.join(worktreePath, normalized);
|
|
102
102
|
if (!existsSync(absPath)) {
|
|
103
103
|
return c.json({ error: `Document not found: ${normalized}` }, 404);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { getDb } from '../db/index.js';
|
|
3
|
+
import { listOrphanWorktrees } from '../services/worktree-service.js';
|
|
2
4
|
import { listBranches, listRemoteBranches } from '../utils/git-ops.js';
|
|
3
5
|
/** Hono sub-router for git-related endpoints (branch listing). */
|
|
4
6
|
const app = new Hono();
|
|
@@ -18,4 +20,23 @@ app.get('/branches', (c) => {
|
|
|
18
20
|
return c.json({ error: message }, 500);
|
|
19
21
|
}
|
|
20
22
|
});
|
|
23
|
+
// GET /api/git/orphan-worktrees?projectPath=<path> — list worktrees of a
|
|
24
|
+
// project that are NOT attached to any Kōbō workspace yet.
|
|
25
|
+
app.get('/orphan-worktrees', (c) => {
|
|
26
|
+
try {
|
|
27
|
+
const projectPath = c.req.query('projectPath');
|
|
28
|
+
if (!projectPath) {
|
|
29
|
+
return c.json({ error: 'Missing required query parameter: projectPath' }, 400);
|
|
30
|
+
}
|
|
31
|
+
const db = getDb();
|
|
32
|
+
const rows = db.prepare('SELECT worktree_path FROM workspaces WHERE project_path = ?').all(projectPath);
|
|
33
|
+
const attachedPaths = new Set(rows.map((r) => r.worktree_path).filter((p) => !!p));
|
|
34
|
+
const orphans = listOrphanWorktrees(projectPath, attachedPaths);
|
|
35
|
+
return c.json(orphans);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return c.json({ error: message }, 500);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
21
42
|
export default app;
|
|
@@ -45,13 +45,13 @@ app.get('/report', (c) => {
|
|
|
45
45
|
const dbSchemaVersion = row?.v ?? 0;
|
|
46
46
|
// Workspaces + worktrees on-disk check
|
|
47
47
|
const workspaces = db
|
|
48
|
-
.prepare('SELECT id, name, project_path, working_branch, archived_at FROM workspaces')
|
|
48
|
+
.prepare('SELECT id, name, project_path, working_branch, worktree_path, archived_at FROM workspaces')
|
|
49
49
|
.all();
|
|
50
50
|
const worktreesMissing = [];
|
|
51
51
|
for (const ws of workspaces) {
|
|
52
52
|
if (ws.archived_at)
|
|
53
53
|
continue;
|
|
54
|
-
const wtPath = path.join(ws.project_path, '.worktrees', ws.working_branch);
|
|
54
|
+
const wtPath = ws.worktree_path ?? path.join(ws.project_path, '.worktrees', ws.working_branch);
|
|
55
55
|
if (!fs.existsSync(wtPath)) {
|
|
56
56
|
worktreesMissing.push({ workspaceId: ws.id, name: ws.name, path: wtPath, exists: false });
|
|
57
57
|
}
|
|
@@ -38,7 +38,7 @@ app.post('/:id/images', async (c) => {
|
|
|
38
38
|
if (buffer.length > MAX_FILE_SIZE) {
|
|
39
39
|
return c.json({ error: `File too large (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Max: 10 MB` }, 400);
|
|
40
40
|
}
|
|
41
|
-
const worktreePath =
|
|
41
|
+
const worktreePath = workspace.worktreePath;
|
|
42
42
|
const result = await imageService.saveImage(worktreePath, buffer, file.name);
|
|
43
43
|
return c.json({ uid: result.uid, path: result.relativePath }, 201);
|
|
44
44
|
}
|
|
@@ -70,7 +70,7 @@ app.get('/:id/images/file', async (c) => {
|
|
|
70
70
|
if (!/^(\.ai\/images\/|images\/)[^/]/.test(requested) || requested.includes('..')) {
|
|
71
71
|
return c.json({ error: 'Invalid or disallowed image path' }, 400);
|
|
72
72
|
}
|
|
73
|
-
const worktreePath =
|
|
73
|
+
const worktreePath = workspace.worktreePath;
|
|
74
74
|
const imagesRoot = path.resolve(worktreePath, '.ai/images');
|
|
75
75
|
const fullPath = path.resolve(worktreePath, requested);
|
|
76
76
|
// Containment check: fullPath must be a descendant of imagesRoot. `+ path.sep`
|
|
@@ -104,7 +104,7 @@ app.delete('/:id/images/:uid', async (c) => {
|
|
|
104
104
|
if (!workspace) {
|
|
105
105
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
106
106
|
}
|
|
107
|
-
const worktreePath =
|
|
107
|
+
const worktreePath = workspace.worktreePath;
|
|
108
108
|
await imageService.deleteImage(worktreePath, uid);
|
|
109
109
|
return c.body(null, 204);
|
|
110
110
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { refreshNow } from '../services/usage/poller.js';
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
app.post('/:providerId/refresh', async (c) => {
|
|
5
|
+
const providerId = c.req.param('providerId');
|
|
6
|
+
try {
|
|
7
|
+
const snap = await refreshNow(providerId);
|
|
8
|
+
if (!snap) {
|
|
9
|
+
return c.json({ error: `Unknown provider '${providerId}'` }, 404);
|
|
10
|
+
}
|
|
11
|
+
return c.json({ snapshot: snap }, 200);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15
|
+
return c.json({ error: message }, 500);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
export default app;
|