repoburg 1.3.188 → 1.3.196
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/agent-pane/.prettierignore +5 -0
- package/agent-pane/.prettierrc +8 -0
- package/agent-pane/backend/coverage/base.css +224 -0
- package/agent-pane/backend/coverage/block-navigation.js +87 -0
- package/agent-pane/backend/coverage/clover.xml +887 -0
- package/agent-pane/backend/coverage/coverage-final.json +10 -0
- package/agent-pane/backend/coverage/favicon.png +0 -0
- package/agent-pane/backend/coverage/index.html +176 -0
- package/agent-pane/backend/coverage/prettify.css +1 -0
- package/agent-pane/backend/coverage/prettify.js +2 -0
- package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/backend/coverage/sorter.js +210 -0
- package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
- package/agent-pane/backend/coverage/src/codex/index.html +131 -0
- package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
- package/agent-pane/backend/coverage/src/db/index.html +131 -0
- package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
- package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
- package/agent-pane/backend/coverage/src/index.html +116 -0
- package/agent-pane/backend/coverage/src/index.ts.html +262 -0
- package/agent-pane/backend/coverage/src/routes/index.html +146 -0
- package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
- package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
- package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
- package/agent-pane/backend/coverage/src/ws/index.html +116 -0
- package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
- package/agent-pane/backend/data.db +0 -0
- package/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +70869 -0
- package/agent-pane/backend/package-lock.json +4529 -1013
- package/agent-pane/backend/package.json +10 -6
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
- package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
- package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
- package/agent-pane/backend/src/claude/claude-types.ts +33 -0
- package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
- package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
- package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
- package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
- package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
- package/agent-pane/backend/src/codex/codex-provider.ts +521 -265
- package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
- package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
- package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
- package/agent-pane/backend/src/db/index.test.ts +125 -0
- package/agent-pane/backend/src/db/index.ts +39 -8
- package/agent-pane/backend/src/db/schema.ts +28 -74
- package/agent-pane/backend/src/index.ts +29 -2
- package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
- package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
- package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
- package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
- package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
- package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
- package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
- package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
- package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
- package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
- package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
- package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
- package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
- package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
- package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
- package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
- package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
- package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
- package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
- package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
- package/agent-pane/backend/src/providers/index.ts +14 -0
- package/agent-pane/backend/src/providers/registry.test.ts +92 -0
- package/agent-pane/backend/src/providers/registry.ts +43 -0
- package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
- package/agent-pane/backend/src/routes/files.ts +31 -0
- package/agent-pane/backend/src/routes/mcp.ts +33 -0
- package/agent-pane/backend/src/routes/models.test.ts +92 -0
- package/agent-pane/backend/src/routes/models.ts +16 -0
- package/agent-pane/backend/src/routes/plugins.ts +40 -0
- package/agent-pane/backend/src/routes/providers.test.ts +69 -0
- package/agent-pane/backend/src/routes/providers.ts +57 -0
- package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
- package/agent-pane/backend/src/routes/sessions.ts +295 -42
- package/agent-pane/backend/src/routes/skills.test.ts +114 -0
- package/agent-pane/backend/src/routes/skills.ts +70 -0
- package/agent-pane/backend/src/routes/turns.test.ts +152 -0
- package/agent-pane/backend/src/routes/turns.ts +36 -16
- package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
- package/agent-pane/backend/src/routes/workspaces.ts +73 -55
- package/agent-pane/backend/src/ws/index.test.ts +107 -0
- package/agent-pane/backend/src/ws/index.ts +19 -11
- package/agent-pane/backend/tsconfig.json +4 -2
- package/agent-pane/backend/vitest.config.ts +20 -0
- package/agent-pane/eslint.config.js +33 -0
- package/agent-pane/frontend/coverage/base.css +224 -0
- package/agent-pane/frontend/coverage/block-navigation.js +87 -0
- package/agent-pane/frontend/coverage/clover.xml +2962 -0
- package/agent-pane/frontend/coverage/coverage-final.json +37 -0
- package/agent-pane/frontend/coverage/favicon.png +0 -0
- package/agent-pane/frontend/coverage/index.html +251 -0
- package/agent-pane/frontend/coverage/prettify.css +1 -0
- package/agent-pane/frontend/coverage/prettify.js +2 -0
- package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/frontend/coverage/sorter.js +210 -0
- package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
- package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/api/index.html +161 -0
- package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
- package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
- package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
- package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
- package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
- package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
- package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
- package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
- package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
- package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
- package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
- package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
- package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
- package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
- package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
- package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
- package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
- package/agent-pane/frontend/coverage/src/components/index.html +326 -0
- package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
- package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
- package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
- package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
- package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
- package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
- package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
- package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
- package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
- package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
- package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
- package/agent-pane/frontend/coverage/src/index.html +146 -0
- package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
- package/agent-pane/frontend/coverage/src/store/index.html +116 -0
- package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
- package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
- package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
- package/agent-pane/frontend/coverage/src/test/index.html +131 -0
- package/agent-pane/frontend/coverage/src/types/index.html +116 -0
- package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
- package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
- package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
- package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
- package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
- package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
- package/agent-pane/frontend/dist/index.html +19 -0
- package/agent-pane/frontend/package-lock.json +2434 -213
- package/agent-pane/frontend/package.json +10 -3
- package/agent-pane/frontend/src/App.test.tsx +74 -0
- package/agent-pane/frontend/src/App.tsx +1 -1
- package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
- package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
- package/agent-pane/frontend/src/api/client.test.ts +126 -0
- package/agent-pane/frontend/src/api/client.ts +22 -29
- package/agent-pane/frontend/src/api/models.ts +7 -0
- package/agent-pane/frontend/src/api/providers.ts +12 -0
- package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
- package/agent-pane/frontend/src/api/sessions.ts +43 -1
- package/agent-pane/frontend/src/api/skills.ts +12 -0
- package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
- package/agent-pane/frontend/src/api/websocket.ts +27 -2
- package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
- package/agent-pane/frontend/src/api/workspaces.ts +17 -3
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +26 -23
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
- package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
- package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
- package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +18 -25
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
- package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
- package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
- package/agent-pane/frontend/src/components/SessionTabs.tsx +12 -12
- package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
- package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
- package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
- package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
- package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
- package/agent-pane/frontend/src/components/Toast.tsx +2 -6
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
- package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
- package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -39
- package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
- package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +63 -31
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
- package/agent-pane/frontend/src/constants/models.test.ts +97 -0
- package/agent-pane/frontend/src/constants/models.ts +129 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
- package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
- package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
- package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
- package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
- package/agent-pane/frontend/src/main.test.tsx +37 -0
- package/agent-pane/frontend/src/main.tsx +1 -1
- package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
- package/agent-pane/frontend/src/store/store.test.ts +1284 -0
- package/agent-pane/frontend/src/store/store.ts +16 -617
- package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
- package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
- package/agent-pane/frontend/src/styles/app.css +113 -7
- package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
- package/agent-pane/frontend/src/test/fixtures.ts +96 -0
- package/agent-pane/frontend/src/test-setup.ts +5 -0
- package/agent-pane/frontend/src/types/index.test.ts +128 -0
- package/agent-pane/frontend/src/types/index.ts +80 -104
- package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
- package/agent-pane/frontend/src/utils/changes.ts +39 -0
- package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
- package/agent-pane/frontend/src/utils/duration.ts +14 -0
- package/agent-pane/frontend/tsconfig.json +4 -1
- package/agent-pane/frontend/vite.config.ts +6 -0
- package/agent-pane/frontend/vitest.config.ts +29 -0
- package/agent-pane/package-lock.json +1527 -0
- package/agent-pane/package.json +19 -0
- package/agent-pane/shared/provider-types.ts +53 -0
- package/agent-pane/shared/types.ts +107 -0
- package/agentpane-branch-session.png +0 -0
- package/agentpane-expanded-all.png +0 -0
- package/agentpane-fixed-rendering.png +0 -0
- package/agentpane-focus-check.png +0 -0
- package/agentpane-initial.png +0 -0
- package/agentpane-mcp-detail.png +0 -0
- package/agentpane-no-focus-ring.png +0 -0
- package/agentpane-soft-focus.png +0 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/data.db +0 -0
- package/empty-state.png +0 -0
- package/frontend-home.png +0 -0
- package/model-selector-open.png +0 -0
- package/package.json +1 -1
- package/slash-popup-working.png +0 -0
- package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { FastifyInstance } from 'fastify';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import { getDb } from '../db/index.ts';
|
|
4
|
+
import { getProvider } from '../providers/registry.ts';
|
|
4
5
|
import type { Session, SessionDetail, Turn, Item } from '../db/schema.ts';
|
|
5
6
|
|
|
6
|
-
/** Raw DB row for turns — token_usage is a JSON string, not parsed. */
|
|
7
|
-
interface TurnRow extends Omit<Turn, 'token_usage'> {
|
|
7
|
+
/** Raw DB row for turns — token_usage is a JSON string, not parsed; no items field. */
|
|
8
|
+
interface TurnRow extends Omit<Turn, 'token_usage' | 'items'> {
|
|
8
9
|
token_usage: string | null;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -30,10 +31,14 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
|
|
|
30
31
|
return reply.code(404).send({ error: 'Session not found' });
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const turnRows = db
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const turnRows = db
|
|
35
|
+
.prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC')
|
|
36
|
+
.all(id) as TurnRow[];
|
|
37
|
+
const itemRows = db
|
|
38
|
+
.prepare(
|
|
39
|
+
'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) ORDER BY created_at ASC',
|
|
40
|
+
)
|
|
41
|
+
.all(id) as ItemRow[];
|
|
37
42
|
|
|
38
43
|
// Parse item data from JSON string and group by turn_id
|
|
39
44
|
const itemsByTurn = new Map<string, Item[]>();
|
|
@@ -60,22 +65,44 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
|
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
// POST /api/sessions — create session
|
|
63
|
-
fastify.post<{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
fastify.post<{
|
|
69
|
+
Body: { title?: string; runtime_type?: string; model?: string; model_reasoning_effort?: string; cwd?: string; runtime_config?: string };
|
|
70
|
+
}>('/api/sessions', {
|
|
71
|
+
schema: {
|
|
72
|
+
body: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
title: { type: 'string' },
|
|
76
|
+
runtime_type: { type: 'string' },
|
|
77
|
+
model: { type: ['string', 'null'] },
|
|
78
|
+
model_reasoning_effort: { type: ['string', 'null'] },
|
|
79
|
+
cwd: { type: ['string', 'null'] },
|
|
80
|
+
runtime_config: { type: ['string', 'null'] },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}, async (request) => {
|
|
85
|
+
const {
|
|
86
|
+
title = null,
|
|
87
|
+
runtime_type = 'codex',
|
|
88
|
+
model = null,
|
|
89
|
+
model_reasoning_effort = null,
|
|
90
|
+
cwd = null,
|
|
91
|
+
runtime_config = null,
|
|
92
|
+
} = request.body || {};
|
|
93
|
+
const id = uuidv4();
|
|
94
|
+
const now = new Date().toISOString();
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
getDb()
|
|
97
|
+
.prepare(
|
|
98
|
+
`INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, created_at, updated_at, runtime_config)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, ?, ?, ?)`,
|
|
100
|
+
)
|
|
101
|
+
.run(id, title, runtime_type, model, model_reasoning_effort, cwd, now, now, runtime_config);
|
|
74
102
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
103
|
+
const session = getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
104
|
+
return session;
|
|
105
|
+
});
|
|
79
106
|
|
|
80
107
|
// DELETE /api/sessions/:id — delete session (cascade)
|
|
81
108
|
fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (request, reply) => {
|
|
@@ -95,40 +122,266 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
|
|
|
95
122
|
return { success: true };
|
|
96
123
|
});
|
|
97
124
|
|
|
98
|
-
// PATCH /api/sessions/:id — update session (title, lifecycle, has_unreviewed)
|
|
99
|
-
fastify.patch<{
|
|
100
|
-
|
|
125
|
+
// PATCH /api/sessions/:id — update session (title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config)
|
|
126
|
+
fastify.patch<{
|
|
127
|
+
Params: { id: string };
|
|
128
|
+
Body: {
|
|
129
|
+
title?: string;
|
|
130
|
+
lifecycle?: string;
|
|
131
|
+
has_unreviewed?: number;
|
|
132
|
+
model?: string | null;
|
|
133
|
+
model_reasoning_effort?: string | null;
|
|
134
|
+
runtime_config?: string | null;
|
|
135
|
+
runtime_type?: string;
|
|
136
|
+
};
|
|
137
|
+
}>('/api/sessions/:id', {
|
|
138
|
+
schema: {
|
|
139
|
+
body: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
title: { type: 'string' },
|
|
143
|
+
lifecycle: { type: 'string' },
|
|
144
|
+
has_unreviewed: { type: 'number' },
|
|
145
|
+
model: { type: ['string', 'null'] },
|
|
146
|
+
model_reasoning_effort: { type: ['string', 'null'] },
|
|
147
|
+
runtime_config: { type: ['string', 'null'] },
|
|
148
|
+
runtime_type: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}, async (request, reply) => {
|
|
153
|
+
const { id } = request.params;
|
|
154
|
+
const { title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config, runtime_type } = request.body || {};
|
|
155
|
+
const db = getDb();
|
|
156
|
+
|
|
157
|
+
const existing = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
158
|
+
if (!existing) {
|
|
159
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Block provider switch while a turn is running
|
|
163
|
+
if (runtime_type !== undefined && runtime_type !== existing.runtime_type && existing.is_running) {
|
|
164
|
+
return reply.code(409).send({ error: 'Cannot switch provider while a turn is running' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
const updates: string[] = ['updated_at = ?'];
|
|
169
|
+
const params: (string | number | null)[] = [now];
|
|
170
|
+
|
|
171
|
+
let modelChanged = false;
|
|
172
|
+
|
|
173
|
+
if (title !== undefined) {
|
|
174
|
+
updates.push('title = ?');
|
|
175
|
+
params.push(title);
|
|
176
|
+
}
|
|
177
|
+
if (lifecycle !== undefined) {
|
|
178
|
+
updates.push('lifecycle = ?');
|
|
179
|
+
params.push(lifecycle);
|
|
180
|
+
}
|
|
181
|
+
if (has_unreviewed !== undefined) {
|
|
182
|
+
updates.push('has_unreviewed = ?');
|
|
183
|
+
params.push(has_unreviewed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Provider switch — clear model/reasoning if not explicitly provided
|
|
187
|
+
if (runtime_type !== undefined && runtime_type !== existing.runtime_type) {
|
|
188
|
+
modelChanged = true; // old model is invalid for new provider
|
|
189
|
+
updates.push('runtime_type = ?');
|
|
190
|
+
params.push(runtime_type);
|
|
191
|
+
|
|
192
|
+
// Clear model if not explicitly set in this request
|
|
193
|
+
if (model === undefined) {
|
|
194
|
+
updates.push('model = ?');
|
|
195
|
+
params.push(null);
|
|
196
|
+
}
|
|
197
|
+
// Clear reasoning if not explicitly set
|
|
198
|
+
if (model_reasoning_effort === undefined) {
|
|
199
|
+
updates.push('model_reasoning_effort = ?');
|
|
200
|
+
params.push(null);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (model !== undefined) {
|
|
205
|
+
updates.push('model = ?');
|
|
206
|
+
params.push(model);
|
|
207
|
+
if (model !== existing.model) modelChanged = true;
|
|
208
|
+
}
|
|
209
|
+
if (model_reasoning_effort !== undefined) {
|
|
210
|
+
updates.push('model_reasoning_effort = ?');
|
|
211
|
+
params.push(model_reasoning_effort);
|
|
212
|
+
if (model_reasoning_effort !== existing.model_reasoning_effort) modelChanged = true;
|
|
213
|
+
}
|
|
214
|
+
if (runtime_config !== undefined) {
|
|
215
|
+
updates.push('runtime_config = ?');
|
|
216
|
+
params.push(runtime_config);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
db.prepare(`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`).run(...params, id);
|
|
220
|
+
|
|
221
|
+
// If provider or model changed, clear the cached thread and clean up old provider
|
|
222
|
+
if (modelChanged && existing.external_thread_id) {
|
|
223
|
+
db.prepare('UPDATE sessions SET external_thread_id = NULL, updated_at = ? WHERE id = ?').run(now, id);
|
|
224
|
+
// Use old provider to clear its in-memory thread state
|
|
225
|
+
getProvider(existing.runtime_type).clearThread(id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
229
|
+
return session;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// POST /api/sessions/:id/rewind — delete turns after target turn, reset Codex thread
|
|
233
|
+
fastify.post<{ Params: { id: string }; Body: { turn_id: string } }>(
|
|
234
|
+
'/api/sessions/:id/rewind',
|
|
235
|
+
{
|
|
236
|
+
schema: {
|
|
237
|
+
body: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
required: ['turn_id'],
|
|
240
|
+
properties: {
|
|
241
|
+
turn_id: { type: 'string' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
101
246
|
async (request, reply) => {
|
|
102
247
|
const { id } = request.params;
|
|
103
|
-
const {
|
|
104
|
-
|
|
248
|
+
const { turn_id } = request.body || {};
|
|
249
|
+
if (!turn_id) {
|
|
250
|
+
return reply.code(400).send({ error: 'turn_id is required' });
|
|
251
|
+
}
|
|
105
252
|
|
|
106
|
-
const
|
|
107
|
-
|
|
253
|
+
const db = getDb();
|
|
254
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
255
|
+
if (!session) {
|
|
108
256
|
return reply.code(404).send({ error: 'Session not found' });
|
|
109
257
|
}
|
|
258
|
+
if (session.is_running) {
|
|
259
|
+
return reply.code(409).send({ error: 'Cannot rewind while a turn is running' });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const targetTurn = db.prepare('SELECT created_at FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
|
|
263
|
+
{ created_at: string } | undefined;
|
|
264
|
+
if (!targetTurn) {
|
|
265
|
+
return reply.code(404).send({ error: 'Turn not found' });
|
|
266
|
+
}
|
|
110
267
|
|
|
268
|
+
// Delete turns after the target (cascade deletes items via FK)
|
|
269
|
+
db.prepare('DELETE FROM turns WHERE session_id = ? AND created_at > ?').run(id, targetTurn.created_at);
|
|
270
|
+
|
|
271
|
+
// Null out external_thread_id — next turn creates a fresh Codex thread
|
|
111
272
|
const now = new Date().toISOString();
|
|
112
|
-
|
|
113
|
-
|
|
273
|
+
db.prepare('UPDATE sessions SET external_thread_id = NULL, updated_at = ? WHERE id = ?').run(now, id);
|
|
274
|
+
|
|
275
|
+
// Clear in-memory thread entry
|
|
276
|
+
getProvider(session.runtime_type).clearThread(id);
|
|
114
277
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
278
|
+
const updated = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
|
|
279
|
+
return updated;
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// POST /api/sessions/:id/branch — create new session with turns up to and including target turn
|
|
284
|
+
fastify.post<{ Params: { id: string }; Body: { turn_id: string; title?: string } }>(
|
|
285
|
+
'/api/sessions/:id/branch',
|
|
286
|
+
{
|
|
287
|
+
schema: {
|
|
288
|
+
body: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
required: ['turn_id'],
|
|
291
|
+
properties: {
|
|
292
|
+
turn_id: { type: 'string' },
|
|
293
|
+
title: { type: 'string' },
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
async (request, reply) => {
|
|
299
|
+
const { id } = request.params;
|
|
300
|
+
const { turn_id, title } = request.body || {};
|
|
301
|
+
if (!turn_id) {
|
|
302
|
+
return reply.code(400).send({ error: 'turn_id is required' });
|
|
118
303
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
304
|
+
|
|
305
|
+
const db = getDb();
|
|
306
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
307
|
+
if (!session) {
|
|
308
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
122
309
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
310
|
+
|
|
311
|
+
const targetTurn = db.prepare('SELECT created_at FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
|
|
312
|
+
{ created_at: string } | undefined;
|
|
313
|
+
if (!targetTurn) {
|
|
314
|
+
return reply.code(404).send({ error: 'Turn not found' });
|
|
126
315
|
}
|
|
127
316
|
|
|
128
|
-
|
|
317
|
+
// Create new session
|
|
318
|
+
const newSessionId = uuidv4();
|
|
319
|
+
const now = new Date().toISOString();
|
|
320
|
+
const newTitle = title || `Branch of ${session.title || session.id.slice(0, 8)}`;
|
|
321
|
+
|
|
322
|
+
db.prepare(
|
|
323
|
+
`INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, lifecycle, has_unreviewed, created_at, updated_at, runtime_config)
|
|
324
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, 'working', 0, ?, ?, ?)`,
|
|
325
|
+
).run(
|
|
326
|
+
newSessionId,
|
|
327
|
+
newTitle,
|
|
328
|
+
session.runtime_type,
|
|
329
|
+
session.model,
|
|
330
|
+
session.model_reasoning_effort,
|
|
331
|
+
session.cwd,
|
|
332
|
+
now,
|
|
333
|
+
now,
|
|
334
|
+
session.runtime_config,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Copy turns up to and including the target turn
|
|
338
|
+
const turnRows = db
|
|
339
|
+
.prepare('SELECT * FROM turns WHERE session_id = ? AND created_at <= ? ORDER BY created_at ASC')
|
|
340
|
+
.all(id, targetTurn.created_at) as TurnRow[];
|
|
341
|
+
|
|
342
|
+
for (const turn of turnRows) {
|
|
343
|
+
const newTurnId = uuidv4();
|
|
344
|
+
db.prepare(
|
|
345
|
+
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
|
|
346
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
347
|
+
).run(
|
|
348
|
+
newTurnId,
|
|
349
|
+
newSessionId,
|
|
350
|
+
turn.user_prompt,
|
|
351
|
+
turn.agent_response,
|
|
352
|
+
turn.thoughts,
|
|
353
|
+
turn.token_usage,
|
|
354
|
+
turn.external_turn_id,
|
|
355
|
+
turn.status,
|
|
356
|
+
turn.error,
|
|
357
|
+
turn.created_at,
|
|
358
|
+
turn.updated_at,
|
|
359
|
+
);
|
|
129
360
|
|
|
130
|
-
|
|
131
|
-
|
|
361
|
+
// Copy items for this turn
|
|
362
|
+
const itemRows = db
|
|
363
|
+
.prepare('SELECT * FROM items WHERE turn_id = ? ORDER BY created_at ASC')
|
|
364
|
+
.all(turn.id) as ItemRow[];
|
|
365
|
+
for (const item of itemRows) {
|
|
366
|
+
const newItemId = uuidv4();
|
|
367
|
+
db.prepare(
|
|
368
|
+
`INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at)
|
|
369
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
370
|
+
).run(
|
|
371
|
+
newItemId,
|
|
372
|
+
newTurnId,
|
|
373
|
+
item.external_item_id,
|
|
374
|
+
item.type,
|
|
375
|
+
item.data,
|
|
376
|
+
item.status,
|
|
377
|
+
item.created_at,
|
|
378
|
+
item.updated_at,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const newSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(newSessionId) as Session;
|
|
384
|
+
return newSession;
|
|
132
385
|
},
|
|
133
386
|
);
|
|
134
|
-
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import Fastify, { type FastifyInstance } from 'fastify';
|
|
3
|
+
|
|
4
|
+
// Mock shared-app-server to return a mock client
|
|
5
|
+
const mockRequest = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock('../codex/shared-app-server.ts', () => ({
|
|
8
|
+
getSharedAppServer: vi.fn(() => ({
|
|
9
|
+
request: mockRequest,
|
|
10
|
+
isClosed: vi.fn(() => false),
|
|
11
|
+
initialize: vi.fn(() => Promise.resolve()),
|
|
12
|
+
close: vi.fn(() => Promise.resolve()),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock node:fs for readFileSync used in GET /api/skills/:name
|
|
17
|
+
vi.mock('node:fs', () => ({
|
|
18
|
+
readFileSync: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { registerSkillRoutes } from './skills.ts';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import type { SkillsListResult } from '../codex/app-server-types.ts';
|
|
24
|
+
|
|
25
|
+
const MOCK_SKILLS_RESULT: SkillsListResult = {
|
|
26
|
+
data: [
|
|
27
|
+
{
|
|
28
|
+
cwd: '/tmp',
|
|
29
|
+
skills: [
|
|
30
|
+
{
|
|
31
|
+
name: 'cloudflare',
|
|
32
|
+
description: 'Cloudflare skill',
|
|
33
|
+
path: '/home/.codex/skills/cloudflare/SKILL.md',
|
|
34
|
+
scope: 'user',
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'imagegen',
|
|
39
|
+
description: 'Image generation skill',
|
|
40
|
+
path: '/home/.codex/skills/.system/imagegen/SKILL.md',
|
|
41
|
+
scope: 'system',
|
|
42
|
+
enabled: true,
|
|
43
|
+
interface: {
|
|
44
|
+
displayName: 'Image Gen',
|
|
45
|
+
shortDescription: 'Generate images',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
errors: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('Skill routes', () => {
|
|
55
|
+
let app: FastifyInstance;
|
|
56
|
+
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
|
|
60
|
+
app = Fastify();
|
|
61
|
+
await registerSkillRoutes(app);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('GET /api/skills', () => {
|
|
65
|
+
it('returns skills from app-server', async () => {
|
|
66
|
+
const res = await app.inject({ method: 'GET', url: '/api/skills' });
|
|
67
|
+
expect(res.statusCode).toBe(200);
|
|
68
|
+
const skills = res.json();
|
|
69
|
+
expect(skills).toHaveLength(2);
|
|
70
|
+
expect(skills[0].name).toBe('cloudflare');
|
|
71
|
+
expect(skills[0].type).toBe('skill');
|
|
72
|
+
expect(skills[0].metadata.scope).toBe('user');
|
|
73
|
+
expect(skills[1].name).toBe('Image Gen');
|
|
74
|
+
expect(skills[1].metadata.scope).toBe('system');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('calls skills/list with cwd from query', async () => {
|
|
78
|
+
await app.inject({ method: 'GET', url: '/api/skills?cwd=/custom/path' });
|
|
79
|
+
expect(mockRequest).toHaveBeenCalledWith('skills/list', { cwds: ['/custom/path'] });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('defaults cwd to process.cwd()', async () => {
|
|
83
|
+
await app.inject({ method: 'GET', url: '/api/skills' });
|
|
84
|
+
expect(mockRequest).toHaveBeenCalledWith('skills/list', {
|
|
85
|
+
cwds: [process.cwd()],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns empty array when app-server returns no skills', async () => {
|
|
90
|
+
mockRequest.mockResolvedValue({ data: [{ cwd: '/tmp', skills: [], errors: [] }] });
|
|
91
|
+
const res = await app.inject({ method: 'GET', url: '/api/skills' });
|
|
92
|
+
expect(res.statusCode).toBe(200);
|
|
93
|
+
expect(res.json()).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('GET /api/skills/:name', () => {
|
|
98
|
+
it('returns 404 for unknown skill', async () => {
|
|
99
|
+
mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
|
|
100
|
+
const res = await app.inject({ method: 'GET', url: '/api/skills/nonexistent' });
|
|
101
|
+
expect(res.statusCode).toBe(404);
|
|
102
|
+
expect(res.json()).toEqual({ error: 'Skill not found' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns SKILL.md content for known skill', async () => {
|
|
106
|
+
vi.mocked(readFileSync).mockReturnValue('# Cloudflare\nSkill content here');
|
|
107
|
+
const res = await app.inject({ method: 'GET', url: '/api/skills/cloudflare' });
|
|
108
|
+
expect(res.statusCode).toBe(200);
|
|
109
|
+
expect(res.json().name).toBe('cloudflare');
|
|
110
|
+
expect(res.json().content).toContain('# Cloudflare');
|
|
111
|
+
expect(res.json().path).toBe('/home/.codex/skills/cloudflare/SKILL.md');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import type { Extension } from '@agentpane/shared';
|
|
4
|
+
import { getSharedAppServer } from '../codex/shared-app-server.ts';
|
|
5
|
+
import type { SkillsListResult, AppServerSkill } from '../codex/app-server-types.ts';
|
|
6
|
+
|
|
7
|
+
/** Map an app-server skill to the shared Extension type. */
|
|
8
|
+
function mapSkill(skill: AppServerSkill): Extension {
|
|
9
|
+
return {
|
|
10
|
+
id: skill.name,
|
|
11
|
+
name: skill.interface?.displayName ?? skill.name,
|
|
12
|
+
description: skill.shortDescription ?? skill.interface?.shortDescription ?? skill.description,
|
|
13
|
+
type: 'skill',
|
|
14
|
+
enabled: skill.enabled,
|
|
15
|
+
metadata: {
|
|
16
|
+
scope: skill.scope,
|
|
17
|
+
path: skill.path,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Register skill routes. */
|
|
23
|
+
export async function registerSkillRoutes(fastify: FastifyInstance): Promise<void> {
|
|
24
|
+
// GET /api/skills — list all skills from the app-server
|
|
25
|
+
fastify.get<{ Querystring: { cwd?: string } }>('/api/skills', async (request) => {
|
|
26
|
+
const cwd = request.query.cwd ?? process.cwd();
|
|
27
|
+
const result = await getSharedAppServer().request<SkillsListResult>('skills/list', { cwds: [cwd] });
|
|
28
|
+
|
|
29
|
+
// Flatten all skills from all cwds in the response
|
|
30
|
+
const allSkills: Extension[] = [];
|
|
31
|
+
for (const entry of result.data) {
|
|
32
|
+
for (const skill of entry.skills) {
|
|
33
|
+
allSkills.push(mapSkill(skill));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return allSkills;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// GET /api/skills/:name — get full SKILL.md content
|
|
40
|
+
fastify.get<{ Params: { name: string }; Querystring: { cwd?: string } }>(
|
|
41
|
+
'/api/skills/:name',
|
|
42
|
+
async (request, reply) => {
|
|
43
|
+
const { name } = request.params;
|
|
44
|
+
const cwd = request.query.cwd ?? process.cwd();
|
|
45
|
+
|
|
46
|
+
// Fetch skills list to find the path for this skill
|
|
47
|
+
const result = await getSharedAppServer().request<SkillsListResult>('skills/list', { cwds: [cwd] });
|
|
48
|
+
|
|
49
|
+
let skillPath: string | null = null;
|
|
50
|
+
for (const entry of result.data) {
|
|
51
|
+
const skill = entry.skills.find((s) => s.name === name);
|
|
52
|
+
if (skill) {
|
|
53
|
+
skillPath = skill.path;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!skillPath) {
|
|
59
|
+
return reply.code(404).send({ error: 'Skill not found' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
64
|
+
return { name, content, path: skillPath };
|
|
65
|
+
} catch {
|
|
66
|
+
return reply.code(500).send({ error: 'Failed to read skill' });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|