@loicngr/kobo 1.11.2 → 1.11.4
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/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/dist/server/db/migrations.js +20 -0
- package/dist/server/db/schema.js +1 -0
- package/dist/server/routes/workspaces.js +71 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
- package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -0
- package/dist/server/services/agent/engines/codex/capabilities.js +2 -1
- package/dist/server/services/agent/engines/codex/engine.js +1 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -2
- package/dist/server/services/agent/orchestrator.js +45 -8
- package/dist/server/services/engine-handoff-service.js +41 -0
- package/dist/server/services/forge/bitbucket/provider.js +291 -0
- package/dist/server/services/forge/registry.js +3 -1
- package/dist/server/services/forge/resolve.js +4 -1
- package/dist/server/services/settings-defaults.js +1 -1
- package/dist/server/services/settings-service.js +15 -1
- package/dist/server/services/workspace-service.js +50 -2
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-BNhy8oDx.js → ActivityFeed-CoE7FTe_.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-eRWLuQLR.js → ChangelogPage-Cr_15zCy.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-Cc2YwliF.js → ClosePopup-DQgUw5Fk.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-B9Mxi7zD.js → CreatePage-BgDusUKR.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-DglPAzaZ.js → DiffViewer-CfOhZMXv.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-sS-N-_9T.js → HealthPage-3GHu-I59.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-BoZsUoGd.css → MainLayout-BY-xOjDK.css} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-BelFb6SW.js → MainLayout-DPLjufY9.js} +2 -2
- package/src/client/dist/pwa/assets/{QBtnToggle-eC2_l1xJ.js → QBtnToggle-CC3TCOK8.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-BT8qyt_p.js → QChip-CLKcMuqJ.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-Bj-wqU82.js → QSelect-DUCHcz_p.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-CCNfQ3sa.js → QTooltip-CUM1JiWx.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-6CSIDDcM.js → SearchPage-f9Bjw2s3.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BmTlGyff.js +16 -0
- package/src/client/dist/pwa/assets/{SettingsPage-CENUP7ua.css → SettingsPage-Os-6b-6H.css} +1 -1
- package/src/client/dist/pwa/assets/{ThinkingItem-kP-lU251.js → ThinkingItem-D9OZZN0H.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-DVggdTB9.js → TouchPan-DvWjWFLZ.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-D-1HoeQg.css → WorkspacePage-BbJM8PyI.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-CtiULAL9.js +4 -0
- package/src/client/dist/pwa/assets/{build-path-tree-FNTYv-td.js → build-path-tree-nLp-H-pQ.js} +1 -1
- package/src/client/dist/pwa/assets/{cssMode-DHVn7kI3.js → cssMode-7vYc6lG_.js} +1 -1
- package/src/client/dist/pwa/assets/{documents-DoEDCa8X.js → documents-CSi0kXxo.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api2-DO15WOFV.js → editor.api2-BzgKTMyb.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.main-Ce_TPxw1.js → editor.main-DOAM9U2f.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-DnNNlpvQ.js → expand-template-CT5wSSEr.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BBT0Ye5P.js → formatters-66ZLwD6M.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-B5mFQ9MW.js → freemarker2-CgDldzFl.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-TGXWWwrU.js → handlebars-Dt0cB0x6.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BcoyXTgh.js → html-Blz0fA0T.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-Dhuwig6w.js → htmlMode-CgzmQ96i.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BjRW3rME.js +1 -0
- package/src/client/dist/pwa/assets/index-C0ylRsyw.js +84 -0
- package/src/client/dist/pwa/assets/{javascript-v_uXBoLN.js → javascript-DVKtVeCf.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-CErvRpqG.js → jsonMode-Cfa435aJ.js} +1 -1
- package/src/client/dist/pwa/assets/{kobo-commands-DXJ85QAa.js → kobo-commands-C-rMOa-X.js} +1 -1
- package/src/client/dist/pwa/assets/{layout-Di93C8KP.js → layout-BUYxQPjS.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-ZappZiY7.js → liquid-DU4LoIen.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-ur5VVJZY.js → lspLanguageFeatures-DDmy9kyx.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-BKL2jNxT.js → mdx-CVPu9G6b.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-WfNiFCn_.js → monaco.contribution-BCKXe1LM.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-O2hz04zH.js → network-auth-BP0-h4VQ.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-CysqNMDL.js → pending-diff-open-BAyLIcQ6.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-Crg3_WdF.js → permissionModes-CNIg8DwA.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-DVafgHFe.js → project-color-B38mkxVB.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DXpnDgWC.js → python-WDhDBOmq.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-2pTtVGVn.js → razor-Dv_KM7xr.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-BbmsqP4r.js → render-chat-markdown-DahMoI2g.js} +1 -1
- package/src/client/dist/pwa/assets/{tsMode-t5L5YTTB.js → tsMode-B3UNfmkB.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-C2FUrxc2.js → typescript-CNkrDBQS.js} +1 -1
- package/src/client/dist/pwa/assets/{use-onboarding-CxQRTetV.js → use-onboarding-Bb-Hawzx.js} +1 -1
- package/src/client/dist/pwa/assets/{use-quasar-CUBX9V4F.js → use-quasar-DCRxdLu-.js} +1 -1
- package/src/client/dist/pwa/assets/{workers-ri7So4GA.js → workers-UUdizQoG.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-CvWFj3be.js → xml-zRWWyHRk.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DsSH3Hc4.js → yaml-Uma2sS3z.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-jGIZfB7N.js +0 -16
- package/src/client/dist/pwa/assets/WorkspacePage-BWRpD_9V.js +0 -4
- package/src/client/dist/pwa/assets/i18n-BHXcyfLT.js +0 -1
- package/src/client/dist/pwa/assets/index-CGnQPjwo.js +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.11.4
|
|
8
|
+
|
|
9
|
+
- feat(workspace): switch agent engines within a workspace
|
|
10
|
+
|
|
11
|
+
## 1.11.3
|
|
12
|
+
|
|
13
|
+
- feat(forge): add bitbucket community support
|
|
14
|
+
|
|
7
15
|
## 1.11.2
|
|
8
16
|
|
|
9
17
|
- feat(integrations): let Notion and Sentry be enabled independently, keep
|
package/README.md
CHANGED
|
@@ -16,12 +16,12 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
|
|
19
|
-
- **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`),
|
|
20
|
-
- **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. `Ctrl+K` opens a command palette for common workspace actions. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
|
|
19
|
+
- **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`). Switch between them in the same workspace without creating another worktree: Kōbō starts a fresh session for the target engine and provides an editable handoff with tasks, Git state, diff summary, and recent context.
|
|
20
|
+
- **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. `Ctrl+K` opens a command palette for common workspace actions. The global search page deep-links to the same location. Sessions retain their engine and can be renamed or permanently deleted with only their linked history. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
|
|
21
21
|
- **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
|
|
22
22
|
|
|
23
23
|

|
|
24
|
-
- **Git panel**: a 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, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation, then Kōbō can offer to delete the merged remote branch. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
|
|
24
|
+
- **Git panel**: a 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, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation, then Kōbō can offer to delete the merged remote branch. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), Bitbucket Community (`bkt`), or no forge, auto-detected from the remote and overridable per project.
|
|
25
25
|
|
|
26
26
|

|
|
27
27
|
- **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI. When a project has no configured server, the panel and MCP tool report that explicitly instead of implying one is running.
|
|
@@ -84,7 +84,7 @@ The most common knobs:
|
|
|
84
84
|
| `ANTHROPIC_API_KEY` | none | Claude Code engine credential (alternative to `claude /login`) |
|
|
85
85
|
| `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
|
|
86
86
|
|
|
87
|
-
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
|
|
87
|
+
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. **Settings → Forge** holds local Bitbucket Community credentials for `bkt`; Kōbō uses them for PR actions and provides them only to agents working in Bitbucket projects. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
|
|
88
88
|
|
|
89
89
|
The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
|
|
90
90
|
|
|
@@ -93,7 +93,7 @@ The full reference (every env var, every setting key, MCP server registration, N
|
|
|
93
93
|
- **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly, so no `claude` binary is required at runtime.
|
|
94
94
|
- **OpenAI Codex.** Run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace and bridges its JSON-RPC stream to the same UI.
|
|
95
95
|
|
|
96
|
-
You pick the engine at workspace creation. Both share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
|
|
96
|
+
You pick the engine at workspace creation and can switch it later from the workspace header. A switch never attempts to resume a Claude conversation in Codex (or the reverse): it stops the active agent, starts a fresh target-engine session in the same worktree, and lets you review the handoff before launch. The new agent can retrieve further context from prior sessions through the read-only `kobo__read_workspace_events_csv` and `kobo__search_codebase` MCP tools. Both engines share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
|
|
97
97
|
|
|
98
98
|
## Disk-space purge
|
|
99
99
|
|
|
@@ -441,6 +441,26 @@ export const migrations = [
|
|
|
441
441
|
`);
|
|
442
442
|
},
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
version: 33,
|
|
446
|
+
name: 'add-agent-session-engine',
|
|
447
|
+
migrate: (db) => {
|
|
448
|
+
const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'agent_sessions'").get();
|
|
449
|
+
if (!table)
|
|
450
|
+
return;
|
|
451
|
+
const cols = db.prepare('PRAGMA table_info(agent_sessions)').all();
|
|
452
|
+
if (!cols.some((col) => col.name === 'engine')) {
|
|
453
|
+
db.exec('ALTER TABLE agent_sessions ADD COLUMN engine TEXT');
|
|
454
|
+
}
|
|
455
|
+
// Historical rows predate per-session engine tracking. The workspace
|
|
456
|
+
// value is the best available attribution and keeps their history useful.
|
|
457
|
+
db.exec(`
|
|
458
|
+
UPDATE agent_sessions
|
|
459
|
+
SET engine = (SELECT engine FROM workspaces WHERE workspaces.id = agent_sessions.workspace_id)
|
|
460
|
+
WHERE engine IS NULL
|
|
461
|
+
`);
|
|
462
|
+
},
|
|
463
|
+
},
|
|
444
464
|
];
|
|
445
465
|
/** Current schema version — always equals the highest migration version. */
|
|
446
466
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -16,6 +16,7 @@ import { listChatHistory, pushChatHistory } from '../services/chat-history-servi
|
|
|
16
16
|
import { renderCiFixTemplate } from '../services/ci-fix-template-service.js';
|
|
17
17
|
import * as cronService from '../services/cron-service.js';
|
|
18
18
|
import * as devServerService from '../services/dev-server-service.js';
|
|
19
|
+
import { buildEngineHandoff } from '../services/engine-handoff-service.js';
|
|
19
20
|
import { saveWorkspaceFile, shaOf } from '../services/file-editor-service.js';
|
|
20
21
|
import { getForgeProvider } from '../services/forge/registry.js';
|
|
21
22
|
import { resolveForge } from '../services/forge/resolve.js';
|
|
@@ -77,6 +78,61 @@ app.get('/', (c) => {
|
|
|
77
78
|
return c.json({ error: message }, 500);
|
|
78
79
|
}
|
|
79
80
|
});
|
|
81
|
+
// POST /api/workspaces/:id/engine-handoff-preview — build an editable, deterministic handoff.
|
|
82
|
+
app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
|
|
83
|
+
try {
|
|
84
|
+
const id = c.req.param('id');
|
|
85
|
+
const body = await c.req.json();
|
|
86
|
+
const workspace = workspaceService.getWorkspaceWithTasks(id);
|
|
87
|
+
if (!workspace)
|
|
88
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
89
|
+
const engine = listEngines().find((item) => item.id === body.engine);
|
|
90
|
+
if (!engine)
|
|
91
|
+
return c.json({ error: `Unknown engine '${body.engine ?? ''}'` }, 400);
|
|
92
|
+
return c.json({ handoff: buildEngineHandoff(workspace, workspace.engine, engine.id) });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return c.json({ error: err instanceof Error ? err.message : 'Failed to build engine handoff' }, 500);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// POST /api/workspaces/:id/switch-engine — stop the current controller and start a fresh engine session.
|
|
99
|
+
app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
100
|
+
try {
|
|
101
|
+
const id = c.req.param('id');
|
|
102
|
+
const body = await c.req.json();
|
|
103
|
+
const workspace = workspaceService.getWorkspaceWithTasks(id);
|
|
104
|
+
if (!workspace)
|
|
105
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
106
|
+
if (!body.engine || body.engine === workspace.engine)
|
|
107
|
+
return c.json({ error: 'Choose a different engine' }, 400);
|
|
108
|
+
const engine = listEngines().find((item) => item.id === body.engine);
|
|
109
|
+
if (!engine)
|
|
110
|
+
return c.json({ error: `Unknown engine '${body.engine}'` }, 400);
|
|
111
|
+
const model = typeof body.model === 'string' && engine.capabilities.models.some((item) => item.id === body.model)
|
|
112
|
+
? body.model
|
|
113
|
+
: engine.capabilities.models[0]?.id;
|
|
114
|
+
if (!model)
|
|
115
|
+
return c.json({ error: `Engine '${body.engine}' has no available model` }, 400);
|
|
116
|
+
if (!isAgentPermissionMode(body.agentPermissionMode) ||
|
|
117
|
+
!engine.capabilities.permissionModes.includes(body.agentPermissionMode)) {
|
|
118
|
+
return c.json({ error: `Engine '${body.engine}' does not support the selected permission mode` }, 400);
|
|
119
|
+
}
|
|
120
|
+
const effort = typeof body.reasoningEffort === 'string' ? body.reasoningEffort : 'auto';
|
|
121
|
+
const handoff = typeof body.handoff === 'string' && body.handoff.trim()
|
|
122
|
+
? body.handoff.trim().slice(0, 30_000)
|
|
123
|
+
: buildEngineHandoff(workspace, workspace.engine, body.engine);
|
|
124
|
+
if (agentManager.hasController(id))
|
|
125
|
+
agentManager.stopAgent(id);
|
|
126
|
+
const updated = workspaceService.updateWorkspaceEngineConfiguration(id, body.engine, model, effort, body.agentPermissionMode);
|
|
127
|
+
const session = agentManager.startAgent(id, updated.worktreePath, handoff, updated.model, false, updated.agentPermissionMode, undefined, updated.reasoningEffort);
|
|
128
|
+
workspaceService.updateWorkspaceStatus(id, 'executing');
|
|
129
|
+
wsService.emit(id, 'user:message', { content: handoff, sender: 'user', kind: 'engine-handoff' }, session.agentSessionId);
|
|
130
|
+
return c.json({ workspace: workspaceService.getWorkspace(id), sessionId: session.agentSessionId });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return c.json({ error: err instanceof Error ? err.message : 'Failed to switch engine' }, 500);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
80
136
|
// POST /api/workspaces — create workspace
|
|
81
137
|
app.post('/', migrationGuard, async (c) => {
|
|
82
138
|
try {
|
|
@@ -1222,6 +1278,21 @@ app.patch('/:id/sessions/:sessionId', async (c) => {
|
|
|
1222
1278
|
return c.json({ error: message }, 500);
|
|
1223
1279
|
}
|
|
1224
1280
|
});
|
|
1281
|
+
// DELETE /api/workspaces/:id/sessions/:sessionId — remove a stopped session and its history.
|
|
1282
|
+
app.delete('/:id/sessions/:sessionId', migrationGuard, (c) => {
|
|
1283
|
+
try {
|
|
1284
|
+
const workspaceId = c.req.param('id');
|
|
1285
|
+
const sessionId = c.req.param('sessionId');
|
|
1286
|
+
const deleted = workspaceService.deleteSession(sessionId, workspaceId);
|
|
1287
|
+
if (!deleted)
|
|
1288
|
+
return c.json({ error: `Session '${sessionId}' not found` }, 404);
|
|
1289
|
+
return c.json({ ok: true, workspace: workspaceService.getWorkspace(workspaceId) });
|
|
1290
|
+
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1293
|
+
return c.json({ error: message }, message.includes('active session') ? 409 : 500);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1225
1296
|
// POST /api/workspaces/:id/deferred-tool-use/answer — resume a deferred
|
|
1226
1297
|
// AskUserQuestion call by feeding the user's answers back to the SDK.
|
|
1227
1298
|
app.post('/:id/deferred-tool-use/answer', async (c) => {
|
|
@@ -199,6 +199,7 @@ export function createClaudeCodeEngine() {
|
|
|
199
199
|
console.warn(`[claude-engine stderr] ${data}`);
|
|
200
200
|
}
|
|
201
201
|
},
|
|
202
|
+
env: options.env,
|
|
202
203
|
});
|
|
203
204
|
sdkOptions.abortController = abortController;
|
|
204
205
|
// Override the SDK's libc-blind binary resolution on Linux glibc — see
|
|
@@ -3,11 +3,12 @@ export const CODEX_CAPABILITIES = {
|
|
|
3
3
|
models: CODEX_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
4
4
|
effortLevels: [
|
|
5
5
|
{ id: 'auto', label: 'Auto' },
|
|
6
|
-
{ id: '
|
|
6
|
+
{ id: 'none', label: 'None' },
|
|
7
7
|
{ id: 'low', label: 'Low' },
|
|
8
8
|
{ id: 'medium', label: 'Medium' },
|
|
9
9
|
{ id: 'high', label: 'High' },
|
|
10
10
|
{ id: 'xhigh', label: 'Extra High' },
|
|
11
|
+
{ id: 'max', label: 'Maximum' },
|
|
11
12
|
],
|
|
12
13
|
permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
|
|
13
14
|
supportsResume: true,
|
|
@@ -60,7 +60,7 @@ export function createCodexEngine() {
|
|
|
60
60
|
};
|
|
61
61
|
const streamingBatcher = createStreamingBatcher(emitDirect);
|
|
62
62
|
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
63
|
-
const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
|
|
63
|
+
const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
|
|
64
64
|
if (child.stderr) {
|
|
65
65
|
child.stderr.setEncoding('utf8');
|
|
66
66
|
child.stderr.on('data', (chunk) => {
|
|
@@ -41,9 +41,11 @@ export function buildCodexOptions(input) {
|
|
|
41
41
|
if (input.model && input.model !== 'auto') {
|
|
42
42
|
threadParams.model = input.model;
|
|
43
43
|
}
|
|
44
|
-
// Effort: omit when undefined or 'auto'
|
|
44
|
+
// Effort: omit when undefined or 'auto'. `minimal` was emitted by older
|
|
45
|
+
// Kōbō versions; Codex 5.6 rejects it, so preserve old workspaces by
|
|
46
|
+
// translating it to the closest current setting instead of failing a turn.
|
|
45
47
|
if (input.effort && input.effort !== 'auto') {
|
|
46
|
-
threadParams.modelReasoningEffort = input.effort;
|
|
48
|
+
threadParams.modelReasoningEffort = (input.effort === 'minimal' ? 'none' : input.effort);
|
|
47
49
|
}
|
|
48
50
|
// `default_tools_approval_mode: 'auto'` pre-approves the namespace —
|
|
49
51
|
// without it Codex blocks every MCP tool call with "user cancelled".
|
|
@@ -6,6 +6,7 @@ import { unregisterProcess } from '../../utils/process-tracker.js';
|
|
|
6
6
|
import * as autoLoopService from '../auto-loop-service.js';
|
|
7
7
|
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
8
8
|
import * as cronService from '../cron-service.js';
|
|
9
|
+
import { resolveForge } from '../forge/resolve.js';
|
|
9
10
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
10
11
|
import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
|
|
11
12
|
import { refreshNow } from '../usage/poller.js';
|
|
@@ -342,12 +343,44 @@ function buildMcpServers(workspaceId) {
|
|
|
342
343
|
},
|
|
343
344
|
];
|
|
344
345
|
}
|
|
346
|
+
function isBitbucketProject(projectPath) {
|
|
347
|
+
try {
|
|
348
|
+
return resolveForge(projectPath) === 'bitbucket-community';
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function buildAgentEnv(projectPath) {
|
|
355
|
+
if (!isBitbucketProject(projectPath))
|
|
356
|
+
return undefined;
|
|
357
|
+
const global = getGlobalSettings();
|
|
358
|
+
if (!global.bitbucketToken)
|
|
359
|
+
return undefined;
|
|
360
|
+
return {
|
|
361
|
+
...process.env,
|
|
362
|
+
BKT_HOST: 'https://bitbucket.org',
|
|
363
|
+
BKT_TOKEN: global.bitbucketToken,
|
|
364
|
+
BKT_USERNAME: global.bitbucketUsername,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function buildAgentPrompt(prompt, projectPath) {
|
|
368
|
+
if (!projectPath || !isBitbucketProject(projectPath))
|
|
369
|
+
return prompt;
|
|
370
|
+
return [
|
|
371
|
+
'[Kōbō Bitbucket] Bitbucket credentials are available through the `bkt` CLI in this session.',
|
|
372
|
+
'Use `bkt pr edit <number> --body <description>` and `bkt pr comment <number> --body <comment>` for PR updates.',
|
|
373
|
+
'Do not call the Bitbucket REST API with curl or reuse BKT_TOKEN as a Bearer token: Bitbucket Cloud API tokens require the bkt CLI authentication flow.',
|
|
374
|
+
'',
|
|
375
|
+
prompt,
|
|
376
|
+
].join('\n');
|
|
377
|
+
}
|
|
345
378
|
function resolveSessionForResume(workspaceId, existingSessionId, model) {
|
|
346
379
|
const db = getDb();
|
|
347
380
|
let lastSession;
|
|
348
381
|
if (existingSessionId) {
|
|
349
382
|
lastSession = db
|
|
350
|
-
.prepare('SELECT id, engine_session_id FROM agent_sessions WHERE id = ? AND workspace_id = ? AND engine_session_id IS NOT NULL LIMIT 1')
|
|
383
|
+
.prepare('SELECT id, engine_session_id, engine FROM agent_sessions WHERE id = ? AND workspace_id = ? AND engine_session_id IS NOT NULL LIMIT 1')
|
|
351
384
|
.get(existingSessionId, workspaceId);
|
|
352
385
|
if (!lastSession) {
|
|
353
386
|
throw new Error(`Cannot resume session '${existingSessionId}' for workspace '${workspaceId}': ` +
|
|
@@ -356,7 +389,7 @@ function resolveSessionForResume(workspaceId, existingSessionId, model) {
|
|
|
356
389
|
}
|
|
357
390
|
else {
|
|
358
391
|
lastSession = db
|
|
359
|
-
.prepare('SELECT id, engine_session_id FROM agent_sessions WHERE workspace_id = ? AND engine_session_id IS NOT NULL ORDER BY started_at DESC LIMIT 1')
|
|
392
|
+
.prepare('SELECT id, engine_session_id, engine FROM agent_sessions WHERE workspace_id = ? AND engine_session_id IS NOT NULL ORDER BY started_at DESC LIMIT 1')
|
|
360
393
|
.get(workspaceId);
|
|
361
394
|
}
|
|
362
395
|
const engineSessionId = lastSession?.engine_session_id ?? (existingSessionId ? undefined : sessionIds.get(workspaceId));
|
|
@@ -379,19 +412,19 @@ function resolveSessionForResume(workspaceId, existingSessionId, model) {
|
|
|
379
412
|
db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, model, started_at) VALUES (?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', model ?? null, new Date().toISOString());
|
|
380
413
|
return { agentSessionId, engineSessionId: undefined, existed: false };
|
|
381
414
|
}
|
|
382
|
-
function reuseOrCreateFreshSession(workspaceId, existingSessionId, model) {
|
|
415
|
+
function reuseOrCreateFreshSession(workspaceId, existingSessionId, model, engine) {
|
|
383
416
|
const db = getDb();
|
|
384
417
|
if (existingSessionId) {
|
|
385
418
|
const result = db
|
|
386
|
-
.prepare('UPDATE agent_sessions SET status = ?, started_at = ?, ended_at = NULL, model = ? WHERE id = ? AND workspace_id = ?')
|
|
387
|
-
.run('running', new Date().toISOString(), model ?? null, existingSessionId, workspaceId);
|
|
419
|
+
.prepare('UPDATE agent_sessions SET status = ?, started_at = ?, ended_at = NULL, model = ?, engine = ? WHERE id = ? AND workspace_id = ?')
|
|
420
|
+
.run('running', new Date().toISOString(), model ?? null, engine, existingSessionId, workspaceId);
|
|
388
421
|
if (result.changes === 0) {
|
|
389
422
|
throw new Error(`Agent session '${existingSessionId}' not found for workspace '${workspaceId}'`);
|
|
390
423
|
}
|
|
391
424
|
return existingSessionId;
|
|
392
425
|
}
|
|
393
426
|
const agentSessionId = nanoid();
|
|
394
|
-
db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, model, started_at) VALUES (?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', model ?? null, new Date().toISOString());
|
|
427
|
+
db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, engine, status, model, started_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, engine, 'running', model ?? null, new Date().toISOString());
|
|
395
428
|
return agentSessionId;
|
|
396
429
|
}
|
|
397
430
|
// ── Event handler ─────────────────────────────────────────────────────────────
|
|
@@ -753,15 +786,18 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
753
786
|
const r = resolveSessionForResume(workspaceId, existingSessionId, model);
|
|
754
787
|
agentSessionId = r.agentSessionId;
|
|
755
788
|
resumeFromEngineSessionId = r.engineSessionId;
|
|
789
|
+
// A native resume is only used by the currently selected workspace engine.
|
|
790
|
+
// Keep older DB rows attributable even when they were created pre-migration.
|
|
791
|
+
getDb().prepare('UPDATE agent_sessions SET engine = ? WHERE id = ?').run(engineId, agentSessionId);
|
|
756
792
|
}
|
|
757
793
|
else {
|
|
758
|
-
agentSessionId = reuseOrCreateFreshSession(workspaceId, existingSessionId, model);
|
|
794
|
+
agentSessionId = reuseOrCreateFreshSession(workspaceId, existingSessionId, model, engineId);
|
|
759
795
|
}
|
|
760
796
|
const settings = ws ? readEffectiveSettingsSafe(ws.projectPath) : readEffectiveSettingsSafe(workingDir);
|
|
761
797
|
const options = {
|
|
762
798
|
workspaceId,
|
|
763
799
|
workingDir,
|
|
764
|
-
prompt,
|
|
800
|
+
prompt: buildAgentPrompt(prompt, ws?.projectPath),
|
|
765
801
|
model,
|
|
766
802
|
effort: reasoningEffort,
|
|
767
803
|
// Cascade: explicit caller override → workspace setting → 'bypass'.
|
|
@@ -778,6 +814,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
778
814
|
})(),
|
|
779
815
|
settings,
|
|
780
816
|
mcpServers: buildMcpServers(workspaceId),
|
|
817
|
+
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
781
818
|
};
|
|
782
819
|
const controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, ev));
|
|
783
820
|
controllers.set(workspaceId, controller);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { getDb } from '../db/index.js';
|
|
3
|
+
function runGit(worktreePath, args) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync('git', args.split(' '), { cwd: worktreePath, encoding: 'utf8', timeout: 10_000 }).trim();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function recentConversation(workspaceId) {
|
|
12
|
+
const rows = getDb()
|
|
13
|
+
.prepare(`SELECT type, payload FROM ws_events
|
|
14
|
+
WHERE workspace_id = ?
|
|
15
|
+
AND (type = 'user:message' OR (type = 'agent:event' AND json_extract(payload, '$.kind') = 'message:text'))
|
|
16
|
+
ORDER BY rowid DESC LIMIT 6`)
|
|
17
|
+
.all(workspaceId);
|
|
18
|
+
return rows
|
|
19
|
+
.reverse()
|
|
20
|
+
.flatMap((row) => {
|
|
21
|
+
try {
|
|
22
|
+
const payload = JSON.parse(row.payload);
|
|
23
|
+
const text = typeof payload.content === 'string' ? payload.content : typeof payload.text === 'string' ? payload.text : '';
|
|
24
|
+
return text ? [text.slice(0, 1600)] : [];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.join('\n\n');
|
|
31
|
+
}
|
|
32
|
+
/** Build a deterministic, secret-free handoff for a fresh session on another engine. */
|
|
33
|
+
export function buildEngineHandoff(workspace, sourceEngine, targetEngine) {
|
|
34
|
+
const taskLines = workspace.tasks.length
|
|
35
|
+
? workspace.tasks.map((task) => `- [${task.status === 'done' ? 'x' : ' '}] ${task.title}`).join('\n')
|
|
36
|
+
: '- No Kōbō task recorded.';
|
|
37
|
+
const changedFiles = runGit(workspace.worktreePath, 'status --short') || 'No uncommitted changes detected.';
|
|
38
|
+
const diffStat = runGit(workspace.worktreePath, 'diff --stat') || 'No uncommitted diff stat available.';
|
|
39
|
+
const history = recentConversation(workspace.id) || 'No recent conversation message available.';
|
|
40
|
+
return `# Kōbō engine handoff\n\nYou are taking over this workspace from ${sourceEngine} using ${targetEngine}. Work in the existing worktree and verify the repository state before making changes.\n\n## Objective\n${workspace.description ?? workspace.name}\n\n## Tasks\n${taskLines}\n\n## Git state\n- Branch: ${workspace.workingBranch}\n- Base: ${workspace.sourceBranch}\n- Worktree: ${workspace.worktreePath}\n\nChanged files:\n\`\`\`\n${changedFiles}\n\`\`\`\n\nDiff summary:\n\`\`\`\n${diffStat}\n\`\`\`\n\n## Recent conversation\n${history}\n\n## Recover more context\nIf this handoff is insufficient, use \`kobo__read_workspace_events_csv\` to read the previous workspace conversation in pages (optionally filter by \`session_id\`). Use \`kobo__search_codebase\` to find a precise past decision or user request. Read only the context needed, then continue the task.\n`;
|
|
41
|
+
}
|