pikiloop 0.4.0
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/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard API routes: session CRUD, workspace, streaming state.
|
|
3
|
+
*/
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { loadUserConfig } from '../../core/config/user-config.js';
|
|
9
|
+
import { listAgents, listSkills, decodeAttachmentPathParam, resolveAllowedAttachmentPath, rewriteImageBlocksForTransport, } from '../../agent/index.js';
|
|
10
|
+
import { getSessionStatusForBot } from '../../bot/session-status.js';
|
|
11
|
+
import { findPikiloopSession } from '../../agent/session.js';
|
|
12
|
+
import { readAwaitResume } from '../../agent/await-resume.js';
|
|
13
|
+
import { cancelSessionTask, stopSessionTasks, getSessionStreamState, queueDashboardSessionTask, forkDashboardSessionTask, steerSessionTask, interactionSelectOption, interactionSubmitText, interactionSkip, interactionCancel, getInteractionPrompt, } from '../session-control.js';
|
|
14
|
+
import { querySessions, querySessionTail, querySessionMessages, getWorkspaceOverviews, updateSession, linkSessions, buildMigrationContext, exportSession, importSession, deleteSession, loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, } from '../../bot/session-hub.js';
|
|
15
|
+
import { DASHBOARD_PAGINATION } from '../../core/constants.js';
|
|
16
|
+
import { runtime } from '../runtime.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const DEFAULT_SESSION_PAGE_SIZE = DASHBOARD_PAGINATION.defaultPageSize;
|
|
21
|
+
const MAX_SESSION_PAGE_SIZE = DASHBOARD_PAGINATION.maxPageSize;
|
|
22
|
+
function parsePageNumber(value, fallback = 0) {
|
|
23
|
+
const parsed = Number.parseInt(value || '', 10);
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
25
|
+
return fallback;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function parsePageSize(value, fallback = DEFAULT_SESSION_PAGE_SIZE) {
|
|
29
|
+
const parsed = Number.parseInt(value || '', 10);
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
31
|
+
return fallback;
|
|
32
|
+
return Math.min(parsed, MAX_SESSION_PAGE_SIZE);
|
|
33
|
+
}
|
|
34
|
+
function paginateSessionResult(items, page, limit) {
|
|
35
|
+
const total = items.length;
|
|
36
|
+
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
37
|
+
const safePage = Math.min(page, totalPages - 1);
|
|
38
|
+
const start = safePage * limit;
|
|
39
|
+
return {
|
|
40
|
+
sessions: items.slice(start, start + limit),
|
|
41
|
+
page: safePage,
|
|
42
|
+
limit,
|
|
43
|
+
total,
|
|
44
|
+
totalPages,
|
|
45
|
+
hasMore: safePage + 1 < totalPages,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function enrichWithRuntimeStatus(sessions, bot) {
|
|
49
|
+
return sessions.map(session => {
|
|
50
|
+
const status = bot ? getSessionStatusForBot(bot, session) : null;
|
|
51
|
+
const isRunning = status ? status.isRunning : !!session.running;
|
|
52
|
+
// "Waiting on background work" only applies to a session that isn't
|
|
53
|
+
// currently running — surface the marker the agent parked (if any) so the
|
|
54
|
+
// dashboard can show a distinct "waiting" state instead of "completed".
|
|
55
|
+
const awaiting = !isRunning && session.workdir && session.sessionId
|
|
56
|
+
? readAwaitResume(session.workdir, session.agent, session.sessionId)
|
|
57
|
+
: null;
|
|
58
|
+
return {
|
|
59
|
+
...session,
|
|
60
|
+
running: isRunning,
|
|
61
|
+
runState: isRunning ? 'running' : (session.runState === 'running' ? 'incomplete' : session.runState),
|
|
62
|
+
awaiting,
|
|
63
|
+
isCurrent: status?.isCurrent ?? false,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function readStringField(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
69
|
+
}
|
|
70
|
+
function isUploadFile(value) {
|
|
71
|
+
return !!value
|
|
72
|
+
&& typeof value === 'object'
|
|
73
|
+
&& typeof value.arrayBuffer === 'function';
|
|
74
|
+
}
|
|
75
|
+
function extensionForMimeType(mimeType) {
|
|
76
|
+
switch (mimeType.toLowerCase()) {
|
|
77
|
+
case 'image/png': return '.png';
|
|
78
|
+
case 'image/jpeg': return '.jpg';
|
|
79
|
+
case 'image/webp': return '.webp';
|
|
80
|
+
case 'image/gif': return '.gif';
|
|
81
|
+
case 'image/svg+xml': return '.svg';
|
|
82
|
+
default: return '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function sanitizeUploadFileName(rawName, mimeType, index) {
|
|
86
|
+
const baseName = path.basename(rawName || `attachment-${index + 1}`);
|
|
87
|
+
const parsed = path.parse(baseName);
|
|
88
|
+
const safeStem = (parsed.name || `attachment-${index + 1}`)
|
|
89
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
90
|
+
.replace(/^-+|-+$/g, '')
|
|
91
|
+
|| `attachment-${index + 1}`;
|
|
92
|
+
const ext = parsed.ext || extensionForMimeType(mimeType) || '.bin';
|
|
93
|
+
return `${safeStem}${ext.toLowerCase()}`;
|
|
94
|
+
}
|
|
95
|
+
async function materializeUploadedFiles(entries) {
|
|
96
|
+
const files = entries.filter(isUploadFile);
|
|
97
|
+
if (!files.length) {
|
|
98
|
+
return { attachments: [], cleanup: async () => { } };
|
|
99
|
+
}
|
|
100
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pikiloop-dashboard-upload-'));
|
|
101
|
+
try {
|
|
102
|
+
const attachments = [];
|
|
103
|
+
for (const [index, file] of files.entries()) {
|
|
104
|
+
const filename = sanitizeUploadFileName(String(file.name || ''), String(file.type || ''), index);
|
|
105
|
+
const filePath = path.join(tempDir, filename);
|
|
106
|
+
await fs.promises.writeFile(filePath, Buffer.from(await file.arrayBuffer()));
|
|
107
|
+
attachments.push(filePath);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
attachments,
|
|
111
|
+
cleanup: async () => {
|
|
112
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function parseSessionSendRequest(c) {
|
|
122
|
+
const contentType = String(c.req.header('content-type') || '').toLowerCase();
|
|
123
|
+
if (contentType.includes('multipart/form-data')) {
|
|
124
|
+
const form = await c.req.formData();
|
|
125
|
+
const uploads = await materializeUploadedFiles(form.getAll('attachments'));
|
|
126
|
+
return {
|
|
127
|
+
workdir: readStringField(form.get('workdir')),
|
|
128
|
+
agent: readStringField(form.get('agent')),
|
|
129
|
+
sessionId: readStringField(form.get('sessionId')),
|
|
130
|
+
prompt: readStringField(form.get('prompt')),
|
|
131
|
+
model: readStringField(form.get('model')),
|
|
132
|
+
effort: readStringField(form.get('effort')).toLowerCase(),
|
|
133
|
+
workflow: readStringField(form.get('workflow')) === '1',
|
|
134
|
+
attachments: uploads.attachments,
|
|
135
|
+
previousAgent: readStringField(form.get('previousAgent')),
|
|
136
|
+
previousSessionId: readStringField(form.get('previousSessionId')),
|
|
137
|
+
cleanup: uploads.cleanup,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const body = await c.req.json();
|
|
141
|
+
return {
|
|
142
|
+
workdir: readStringField(body?.workdir),
|
|
143
|
+
agent: readStringField(body?.agent),
|
|
144
|
+
sessionId: readStringField(body?.sessionId),
|
|
145
|
+
prompt: readStringField(body?.prompt),
|
|
146
|
+
model: readStringField(body?.model),
|
|
147
|
+
effort: readStringField(body?.effort).toLowerCase(),
|
|
148
|
+
workflow: body?.workflow === true,
|
|
149
|
+
attachments: [],
|
|
150
|
+
previousAgent: readStringField(body?.previousAgent),
|
|
151
|
+
previousSessionId: readStringField(body?.previousSessionId),
|
|
152
|
+
cleanup: async () => { },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Routes
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
const app = new Hono();
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
// Legacy session routes (backward-compat for dashboard-ui)
|
|
161
|
+
// ==========================================================================
|
|
162
|
+
// Sessions per agent: GET /api/sessions/:agent
|
|
163
|
+
app.get('/api/sessions/:agent', async (c) => {
|
|
164
|
+
const agent = c.req.param('agent');
|
|
165
|
+
const config = loadUserConfig();
|
|
166
|
+
const workdir = runtime.getRequestWorkdir(config);
|
|
167
|
+
const page = parsePageNumber(c.req.query('page'));
|
|
168
|
+
const limit = parsePageSize(c.req.query('limit'));
|
|
169
|
+
const botRef = runtime.getBotRef();
|
|
170
|
+
runtime.debug(`[sessions] endpoint=single agent=${agent} resolvedWorkdir=${workdir} exists=${fs.existsSync(workdir)} ` +
|
|
171
|
+
`page=${page} limit=${limit}`);
|
|
172
|
+
const result = await querySessions({ workdir, agent });
|
|
173
|
+
const enriched = enrichWithRuntimeStatus(result.sessions, botRef);
|
|
174
|
+
const paged = paginateSessionResult(enriched, page, limit);
|
|
175
|
+
runtime.debug(`[sessions] endpoint=single agent=${agent} ok=${result.ok} total=${result.total} ` +
|
|
176
|
+
`returned=${paged.sessions.length} error=${result.errors.join('; ') || '(none)'}`);
|
|
177
|
+
return c.json({
|
|
178
|
+
ok: result.ok,
|
|
179
|
+
error: result.errors[0] || null,
|
|
180
|
+
...paged,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// All sessions (swim lane): GET /api/sessions
|
|
184
|
+
app.get('/api/sessions', async (c) => {
|
|
185
|
+
const config = loadUserConfig();
|
|
186
|
+
const workdir = runtime.getRequestWorkdir(config);
|
|
187
|
+
const page = parsePageNumber(c.req.query('page'));
|
|
188
|
+
const limit = parsePageSize(c.req.query('limit'));
|
|
189
|
+
const botRef = runtime.getBotRef();
|
|
190
|
+
runtime.debug(`[sessions] endpoint=all resolvedWorkdir=${workdir} exists=${fs.existsSync(workdir)} ` +
|
|
191
|
+
`page=${page} limit=${limit}`);
|
|
192
|
+
const agents = listAgents().agents.filter(a => a.installed);
|
|
193
|
+
const swimLane = {};
|
|
194
|
+
await Promise.all(agents.map(async (a) => {
|
|
195
|
+
const result = await querySessions({ workdir, agent: a.agent });
|
|
196
|
+
const enriched = enrichWithRuntimeStatus(result.sessions, botRef);
|
|
197
|
+
const paged = paginateSessionResult(enriched, page, limit);
|
|
198
|
+
swimLane[a.agent] = {
|
|
199
|
+
ok: result.ok,
|
|
200
|
+
error: result.errors[0] || null,
|
|
201
|
+
...paged,
|
|
202
|
+
};
|
|
203
|
+
runtime.debug(`[sessions] endpoint=all agent=${a.agent} ok=${result.ok} total=${result.total} ` +
|
|
204
|
+
`returned=${paged.sessions.length} error=${result.errors.join('; ') || '(none)'}`);
|
|
205
|
+
}));
|
|
206
|
+
return c.json(swimLane);
|
|
207
|
+
});
|
|
208
|
+
// Session detail (tail): GET /api/session-detail/:agent/:id
|
|
209
|
+
app.get('/api/session-detail/:agent/:id', async (c) => {
|
|
210
|
+
const agent = c.req.param('agent');
|
|
211
|
+
const sessionId = decodeURIComponent(c.req.param('id'));
|
|
212
|
+
const config = loadUserConfig();
|
|
213
|
+
const workdir = runtime.getRequestWorkdir(config);
|
|
214
|
+
const limit = parseInt(c.req.query('limit') || '6', 10);
|
|
215
|
+
runtime.debug(`[sessions] endpoint=detail agent=${agent} session=${sessionId} limit=${limit} resolvedWorkdir=${workdir} ` +
|
|
216
|
+
`exists=${fs.existsSync(workdir)}`);
|
|
217
|
+
const tail = await querySessionTail({ agent, sessionId, workdir, limit });
|
|
218
|
+
runtime.debug(`[sessions] endpoint=detail agent=${agent} session=${sessionId} ok=${tail.ok} ` +
|
|
219
|
+
`messages=${tail.messages.length} error=${tail.error || '(none)'}`);
|
|
220
|
+
return c.json(tail);
|
|
221
|
+
});
|
|
222
|
+
// ==========================================================================
|
|
223
|
+
// Workspace CRUD
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
app.get('/api/workspaces', (c) => {
|
|
226
|
+
const workspaces = loadWorkspaces();
|
|
227
|
+
// Always include the current runtimeWorkdir, deduplicating by path
|
|
228
|
+
const config = loadUserConfig();
|
|
229
|
+
const rwd = runtime.getRuntimeWorkdir(config);
|
|
230
|
+
if (rwd && !workspaces.some(w => w.path === rwd)) {
|
|
231
|
+
workspaces.unshift({
|
|
232
|
+
path: rwd,
|
|
233
|
+
name: path.basename(rwd),
|
|
234
|
+
order: -1,
|
|
235
|
+
addedAt: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return c.json({ ok: true, workspaces });
|
|
239
|
+
});
|
|
240
|
+
app.post('/api/workspaces', async (c) => {
|
|
241
|
+
try {
|
|
242
|
+
const body = await c.req.json();
|
|
243
|
+
const wsPath = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
244
|
+
if (!wsPath)
|
|
245
|
+
return c.json({ ok: false, error: 'path is required' }, 400);
|
|
246
|
+
const entry = addWorkspace(wsPath, body?.name);
|
|
247
|
+
return c.json({ ok: true, workspace: entry });
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
app.delete('/api/workspaces', async (c) => {
|
|
254
|
+
try {
|
|
255
|
+
const body = await c.req.json();
|
|
256
|
+
const wsPath = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
257
|
+
if (!wsPath)
|
|
258
|
+
return c.json({ ok: false, error: 'path is required' }, 400);
|
|
259
|
+
const removed = removeWorkspace(wsPath);
|
|
260
|
+
return c.json({ ok: true, removed });
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
app.patch('/api/workspaces', async (c) => {
|
|
267
|
+
try {
|
|
268
|
+
const body = await c.req.json();
|
|
269
|
+
const wsPath = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
270
|
+
if (!wsPath)
|
|
271
|
+
return c.json({ ok: false, error: 'path is required' }, 400);
|
|
272
|
+
const updated = updateWorkspace(wsPath, body);
|
|
273
|
+
return c.json({ ok: true, workspace: updated });
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// ==========================================================================
|
|
280
|
+
// Workspace overviews
|
|
281
|
+
// ==========================================================================
|
|
282
|
+
app.get('/api/workspace-overviews', async (c) => {
|
|
283
|
+
try {
|
|
284
|
+
const overviews = await getWorkspaceOverviews();
|
|
285
|
+
return c.json({ ok: true, overviews });
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
// ==========================================================================
|
|
292
|
+
// Session hub operations
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
app.post('/api/session-hub/sessions', async (c) => {
|
|
295
|
+
try {
|
|
296
|
+
const body = await c.req.json();
|
|
297
|
+
const workdir = typeof body?.workdir === 'string' ? body.workdir.trim() : '';
|
|
298
|
+
if (!workdir)
|
|
299
|
+
return c.json({ ok: false, error: 'workdir is required' }, 400);
|
|
300
|
+
const botRef = runtime.getBotRef();
|
|
301
|
+
const result = await querySessions({
|
|
302
|
+
workdir,
|
|
303
|
+
agent: body?.agents,
|
|
304
|
+
userStatus: body?.userStatus,
|
|
305
|
+
limit: body?.limit,
|
|
306
|
+
});
|
|
307
|
+
return c.json({
|
|
308
|
+
...result,
|
|
309
|
+
sessions: enrichWithRuntimeStatus(result.sessions, botRef),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
app.post('/api/session-hub/session/status', async (c) => {
|
|
317
|
+
try {
|
|
318
|
+
const body = await c.req.json();
|
|
319
|
+
const { workdir, agent, sessionId, status } = body || {};
|
|
320
|
+
if (!workdir || !agent || !sessionId || !status) {
|
|
321
|
+
return c.json({ ok: false, error: 'workdir, agent, sessionId, and status are required' }, 400);
|
|
322
|
+
}
|
|
323
|
+
const updated = updateSession(workdir, agent, sessionId, { userStatus: status });
|
|
324
|
+
return c.json({ ok: true, updated });
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
app.post('/api/session-hub/session/note', async (c) => {
|
|
331
|
+
try {
|
|
332
|
+
const body = await c.req.json();
|
|
333
|
+
const { workdir, agent, sessionId, note } = body || {};
|
|
334
|
+
if (!workdir || !agent || !sessionId) {
|
|
335
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
336
|
+
}
|
|
337
|
+
const updated = updateSession(workdir, agent, sessionId, { userNote: note ?? null });
|
|
338
|
+
return c.json({ ok: true, updated });
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
app.post('/api/session-hub/session/delete', async (c) => {
|
|
345
|
+
try {
|
|
346
|
+
const body = await c.req.json();
|
|
347
|
+
const workdir = typeof body?.workdir === 'string' ? body.workdir.trim() : '';
|
|
348
|
+
const agent = typeof body?.agent === 'string' ? body.agent.trim() : '';
|
|
349
|
+
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
350
|
+
const purgeNative = body?.purgeNative === true;
|
|
351
|
+
if (!workdir || !agent || !sessionId) {
|
|
352
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
353
|
+
}
|
|
354
|
+
if (!runtime.isAgent(agent)) {
|
|
355
|
+
return c.json({ ok: false, error: `Unknown agent: ${agent}` }, 400);
|
|
356
|
+
}
|
|
357
|
+
runtime.debug(`[sessions] endpoint=delete agent=${agent} session=${sessionId} workdir=${workdir} purgeNative=${purgeNative}`);
|
|
358
|
+
const result = await deleteSession({ workdir, agent: agent, sessionId, purgeNative });
|
|
359
|
+
if (result.refusedReason === 'session-running') {
|
|
360
|
+
return c.json({ ok: false, error: 'session is still running — stop it first' }, 409);
|
|
361
|
+
}
|
|
362
|
+
return c.json({
|
|
363
|
+
ok: true,
|
|
364
|
+
recordRemoved: result.recordRemoved,
|
|
365
|
+
pikiloopPathsRemoved: result.pikiloopPathsRemoved,
|
|
366
|
+
nativePathsRemoved: result.nativePathsRemoved,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
app.post('/api/session-hub/session/link', async (c) => {
|
|
374
|
+
try {
|
|
375
|
+
const body = await c.req.json();
|
|
376
|
+
if (!body?.a || !body?.b || !body?.workdir) {
|
|
377
|
+
return c.json({ ok: false, error: 'workdir, a: {agent, sessionId}, b: {agent, sessionId} required' }, 400);
|
|
378
|
+
}
|
|
379
|
+
const linked = linkSessions(body.workdir, body.a, body.b);
|
|
380
|
+
return c.json({ ok: true, linked });
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
app.post('/api/session-hub/session/messages', async (c) => {
|
|
387
|
+
try {
|
|
388
|
+
const body = await c.req.json();
|
|
389
|
+
const { workdir, agent, sessionId, lastNTurns, turnOffset, turnLimit } = body || {};
|
|
390
|
+
if (!workdir || !agent || !sessionId) {
|
|
391
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
392
|
+
}
|
|
393
|
+
const rich = body?.rich !== false;
|
|
394
|
+
const result = await querySessionMessages({
|
|
395
|
+
agent,
|
|
396
|
+
sessionId,
|
|
397
|
+
workdir,
|
|
398
|
+
lastNTurns: Number.isFinite(lastNTurns) ? lastNTurns : undefined,
|
|
399
|
+
turnOffset: Number.isFinite(turnOffset) ? turnOffset : undefined,
|
|
400
|
+
turnLimit: Number.isFinite(turnLimit) ? turnLimit : undefined,
|
|
401
|
+
rich,
|
|
402
|
+
});
|
|
403
|
+
return c.json(rewriteSessionImagesForDashboard(result, agent, sessionId));
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
// Rewrite oversized inline image data URLs into attachment HTTP URLs so
|
|
410
|
+
// dashboard JSON payloads stay compact. Small inline images pass through.
|
|
411
|
+
function rewriteSessionImagesForDashboard(result, agent, sessionId) {
|
|
412
|
+
if (!result.richMessages?.length)
|
|
413
|
+
return result;
|
|
414
|
+
const richMessages = result.richMessages.map(message => ({
|
|
415
|
+
...message,
|
|
416
|
+
blocks: rewriteImageBlocksForTransport(message.blocks, { agent, sessionId }),
|
|
417
|
+
}));
|
|
418
|
+
return { ...result, richMessages };
|
|
419
|
+
}
|
|
420
|
+
// Attachment endpoint — serves on-disk images referenced by RichMessage image
|
|
421
|
+
// blocks via opaque base64url path tokens. The allowlist (see images.ts)
|
|
422
|
+
// confines reads to a known set of agent-managed dirs + the session's workdir.
|
|
423
|
+
app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
424
|
+
const agent = c.req.param('agent');
|
|
425
|
+
const sessionId = decodeURIComponent(c.req.param('id'));
|
|
426
|
+
const token = c.req.query('p') || '';
|
|
427
|
+
if (!token)
|
|
428
|
+
return c.json({ ok: false, error: 'missing path parameter' }, 400);
|
|
429
|
+
let requestedPath;
|
|
430
|
+
try {
|
|
431
|
+
requestedPath = decodeAttachmentPathParam(token);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return c.json({ ok: false, error: 'invalid path token' }, 400);
|
|
435
|
+
}
|
|
436
|
+
if (!requestedPath || requestedPath.includes('\0')) {
|
|
437
|
+
return c.json({ ok: false, error: 'invalid path' }, 400);
|
|
438
|
+
}
|
|
439
|
+
// Widen the allowlist with the session's recorded workdir when known —
|
|
440
|
+
// images generated under the project tree resolve cleanly. Session indexes
|
|
441
|
+
// are per-workdir and this URL carries no workdir, so a lookup against the
|
|
442
|
+
// runtime workdir alone misses sessions living in any OTHER registered
|
|
443
|
+
// workspace (the Session Hub renders all of them through this endpoint) —
|
|
444
|
+
// their user-attached images 403'd as "broken image" in the dashboard.
|
|
445
|
+
// Registered workspace roots come from server-side config, never request
|
|
446
|
+
// input, so widening to all of them keeps the same trust boundary and also
|
|
447
|
+
// covers the pending→native id promotion window where no index has the
|
|
448
|
+
// session yet.
|
|
449
|
+
const config = loadUserConfig();
|
|
450
|
+
const fallbackWorkdir = runtime.getRequestWorkdir(config);
|
|
451
|
+
const managed = findPikiloopSession(fallbackWorkdir, agent, sessionId);
|
|
452
|
+
const workdirs = [
|
|
453
|
+
...(managed?.workdir ? [managed.workdir] : []),
|
|
454
|
+
fallbackWorkdir,
|
|
455
|
+
...loadWorkspaces().map(ws => ws.path),
|
|
456
|
+
];
|
|
457
|
+
const resolved = resolveAllowedAttachmentPath(requestedPath, workdirs);
|
|
458
|
+
if (!resolved)
|
|
459
|
+
return c.json({ ok: false, error: 'forbidden' }, 403);
|
|
460
|
+
let stat;
|
|
461
|
+
try {
|
|
462
|
+
stat = fs.statSync(resolved);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return c.json({ ok: false, error: 'not found' }, 404);
|
|
466
|
+
}
|
|
467
|
+
if (!stat.isFile())
|
|
468
|
+
return c.json({ ok: false, error: 'not a file' }, 400);
|
|
469
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
470
|
+
const mime = mimeForExtFallback(ext);
|
|
471
|
+
const bytes = await fs.promises.readFile(resolved);
|
|
472
|
+
// The path is hash-immutable for agent-managed dirs (`ig_<sha>.png`, …) and
|
|
473
|
+
// the session lifecycle keeps the file stable — long cache is safe.
|
|
474
|
+
return c.body(bytes, 200, {
|
|
475
|
+
'Content-Type': mime,
|
|
476
|
+
'Content-Length': String(bytes.length),
|
|
477
|
+
'Cache-Control': 'private, max-age=31536000, immutable',
|
|
478
|
+
'X-Content-Type-Options': 'nosniff',
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
function mimeForExtFallback(ext) {
|
|
482
|
+
switch (ext.toLowerCase()) {
|
|
483
|
+
case '.png': return 'image/png';
|
|
484
|
+
case '.jpg':
|
|
485
|
+
case '.jpeg': return 'image/jpeg';
|
|
486
|
+
case '.gif': return 'image/gif';
|
|
487
|
+
case '.webp': return 'image/webp';
|
|
488
|
+
case '.svg': return 'image/svg+xml';
|
|
489
|
+
default: return 'application/octet-stream';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
app.post('/api/session-hub/migrate', async (c) => {
|
|
493
|
+
try {
|
|
494
|
+
const body = await c.req.json();
|
|
495
|
+
if (!body?.source || !body?.target) {
|
|
496
|
+
return c.json({ ok: false, error: 'source and target are required' }, 400);
|
|
497
|
+
}
|
|
498
|
+
const result = await buildMigrationContext({
|
|
499
|
+
source: body.source,
|
|
500
|
+
target: body.target,
|
|
501
|
+
lastNTurns: body.lastNTurns,
|
|
502
|
+
});
|
|
503
|
+
return c.json(result);
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
app.post('/api/session-hub/export', async (c) => {
|
|
510
|
+
try {
|
|
511
|
+
const body = await c.req.json();
|
|
512
|
+
if (!body?.workdir || !body?.agent || !body?.sessionId) {
|
|
513
|
+
return c.json({ ok: false, error: 'workdir, agent, sessionId are required' }, 400);
|
|
514
|
+
}
|
|
515
|
+
const result = await exportSession({
|
|
516
|
+
workdir: body.workdir,
|
|
517
|
+
agent: body.agent,
|
|
518
|
+
sessionId: body.sessionId,
|
|
519
|
+
format: body.format || 'markdown',
|
|
520
|
+
lastNTurns: body.lastNTurns,
|
|
521
|
+
});
|
|
522
|
+
return c.json(result);
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
app.post('/api/session-hub/import', async (c) => {
|
|
529
|
+
try {
|
|
530
|
+
const body = await c.req.json();
|
|
531
|
+
if (!body?.workdir || !body?.agent || !body?.content) {
|
|
532
|
+
return c.json({ ok: false, error: 'workdir, agent, and content are required' }, 400);
|
|
533
|
+
}
|
|
534
|
+
const result = importSession({
|
|
535
|
+
workdir: body.workdir,
|
|
536
|
+
agent: body.agent,
|
|
537
|
+
content: body.content,
|
|
538
|
+
format: body.format,
|
|
539
|
+
});
|
|
540
|
+
return c.json(result);
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// ==========================================================================
|
|
547
|
+
// Skills
|
|
548
|
+
// ==========================================================================
|
|
549
|
+
app.get('/api/session-hub/skills', (c) => {
|
|
550
|
+
const workdir = c.req.query('workdir') || '';
|
|
551
|
+
if (!workdir)
|
|
552
|
+
return c.json({ ok: false, error: 'workdir query param required' }, 400);
|
|
553
|
+
try {
|
|
554
|
+
const result = listSkills(workdir);
|
|
555
|
+
return c.json({ ok: true, skills: result.skills });
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
return c.json({ ok: false, skills: [], error: e.message }, 500);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
// ==========================================================================
|
|
562
|
+
// Session interaction (send / recall / steer / stream)
|
|
563
|
+
// ==========================================================================
|
|
564
|
+
app.post('/api/session-hub/session/send', async (c) => {
|
|
565
|
+
try {
|
|
566
|
+
const { workdir, agent, sessionId, prompt, model, effort, workflow, attachments, previousAgent, previousSessionId, cleanup } = await parseSessionSendRequest(c);
|
|
567
|
+
const queued = await queueDashboardSessionTask({
|
|
568
|
+
workdir,
|
|
569
|
+
agent,
|
|
570
|
+
sessionId,
|
|
571
|
+
prompt,
|
|
572
|
+
model,
|
|
573
|
+
effort,
|
|
574
|
+
workflow,
|
|
575
|
+
attachments,
|
|
576
|
+
previousAgent: previousAgent || null,
|
|
577
|
+
previousSessionId: previousSessionId || null,
|
|
578
|
+
});
|
|
579
|
+
await cleanup();
|
|
580
|
+
if (!queued.ok) {
|
|
581
|
+
const status = queued.error === 'Bot is not running' ? 503 : 400;
|
|
582
|
+
return c.json(queued, status);
|
|
583
|
+
}
|
|
584
|
+
runtime.debug(`[session-send] queued task=${queued.taskId} session=${queued.sessionKey} attachments=${attachments.length} ` +
|
|
585
|
+
`prompt="${(prompt || '[attachments only]').slice(0, 80)}"`);
|
|
586
|
+
return c.json(queued);
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
// Polling endpoint: GET /api/session-hub/session/stream-state?agent=X&sessionId=Y
|
|
593
|
+
app.get('/api/session-hub/session/stream-state', (c) => {
|
|
594
|
+
const agent = c.req.query('agent') || '';
|
|
595
|
+
const sessionId = c.req.query('sessionId') || '';
|
|
596
|
+
if (!agent || !sessionId) {
|
|
597
|
+
return c.json({ ok: false, error: 'agent and sessionId query params required' }, 400);
|
|
598
|
+
}
|
|
599
|
+
return c.json(getSessionStreamState(agent, sessionId));
|
|
600
|
+
});
|
|
601
|
+
// Fork: branch off a parent session at `atTurn`, queue the new prompt against
|
|
602
|
+
// the freshly forked child. Returns the queued task + the pending child session
|
|
603
|
+
// key so the dashboard can navigate the user into the child immediately.
|
|
604
|
+
app.post('/api/session-hub/session/fork', async (c) => {
|
|
605
|
+
try {
|
|
606
|
+
const body = await c.req.json();
|
|
607
|
+
const { workdir, agent, sessionId, atTurn, prompt, model, effort, attachments } = body || {};
|
|
608
|
+
if (!workdir || !agent || !sessionId || typeof atTurn !== 'number' || !prompt) {
|
|
609
|
+
return c.json({ ok: false, error: 'workdir, agent, sessionId, atTurn (number), and prompt are required' }, 400);
|
|
610
|
+
}
|
|
611
|
+
const queued = forkDashboardSessionTask({
|
|
612
|
+
workdir,
|
|
613
|
+
agent,
|
|
614
|
+
parentSessionId: sessionId,
|
|
615
|
+
atTurn,
|
|
616
|
+
prompt,
|
|
617
|
+
model: model || null,
|
|
618
|
+
effort: effort || null,
|
|
619
|
+
attachments: Array.isArray(attachments) ? attachments : [],
|
|
620
|
+
});
|
|
621
|
+
if (!queued.ok) {
|
|
622
|
+
const status = queued.error === 'Bot is not running' ? 503 : 400;
|
|
623
|
+
return c.json(queued, status);
|
|
624
|
+
}
|
|
625
|
+
runtime.debug(`[session-fork] queued task=${queued.taskId} parent=${agent}:${sessionId} child=${queued.sessionKey} atTurn=${atTurn}`);
|
|
626
|
+
return c.json(queued);
|
|
627
|
+
}
|
|
628
|
+
catch (e) {
|
|
629
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
app.post('/api/session-hub/session/recall', async (c) => {
|
|
633
|
+
try {
|
|
634
|
+
const body = await c.req.json();
|
|
635
|
+
const { taskId } = body || {};
|
|
636
|
+
if (!taskId) {
|
|
637
|
+
return c.json({ ok: false, error: 'taskId is required' }, 400);
|
|
638
|
+
}
|
|
639
|
+
const result = cancelSessionTask(taskId);
|
|
640
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
641
|
+
}
|
|
642
|
+
catch (e) {
|
|
643
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
// Stop only the currently running stream for a session — queued follow-ups
|
|
647
|
+
// are kept so they run next. Takes (agent, sessionId) rather than taskId so it
|
|
648
|
+
// works during the moment after a fresh send where the client hasn't yet
|
|
649
|
+
// learned the streamTaskId.
|
|
650
|
+
app.post('/api/session-hub/session/stop', async (c) => {
|
|
651
|
+
try {
|
|
652
|
+
const body = await c.req.json();
|
|
653
|
+
const { agent, sessionId } = body || {};
|
|
654
|
+
if (!agent || !sessionId) {
|
|
655
|
+
return c.json({ ok: false, error: 'agent and sessionId are required' }, 400);
|
|
656
|
+
}
|
|
657
|
+
const result = stopSessionTasks(agent, sessionId);
|
|
658
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
659
|
+
}
|
|
660
|
+
catch (e) {
|
|
661
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
app.post('/api/session-hub/session/steer', async (c) => {
|
|
665
|
+
try {
|
|
666
|
+
const body = await c.req.json();
|
|
667
|
+
const { taskId } = body || {};
|
|
668
|
+
if (!taskId) {
|
|
669
|
+
return c.json({ ok: false, error: 'taskId is required' }, 400);
|
|
670
|
+
}
|
|
671
|
+
const result = await steerSessionTask(taskId);
|
|
672
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
// ==========================================================================
|
|
679
|
+
// Persistent thread goal (analogous to Codex CLI's `/goal`).
|
|
680
|
+
// ==========================================================================
|
|
681
|
+
app.get('/api/session-hub/session/goal', async (c) => {
|
|
682
|
+
const workdir = c.req.query('workdir') || '';
|
|
683
|
+
const agent = c.req.query('agent') || '';
|
|
684
|
+
const sessionId = c.req.query('sessionId') || '';
|
|
685
|
+
if (!workdir || !agent || !sessionId) {
|
|
686
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId query params required' }, 400);
|
|
687
|
+
}
|
|
688
|
+
const bot = runtime.getBotRef();
|
|
689
|
+
if (!bot)
|
|
690
|
+
return c.json({ ok: false, error: 'bot not attached' }, 503);
|
|
691
|
+
try {
|
|
692
|
+
const goal = await bot.getSessionGoal(workdir, agent, sessionId);
|
|
693
|
+
return c.json({ ok: true, goal });
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
app.post('/api/session-hub/session/goal', async (c) => {
|
|
700
|
+
try {
|
|
701
|
+
const body = await c.req.json();
|
|
702
|
+
const { workdir, agent, sessionId, objective, tokenBudget, modelId, thinkingEffort } = body || {};
|
|
703
|
+
if (!workdir || !agent || !sessionId || typeof objective !== 'string' || !objective.trim()) {
|
|
704
|
+
return c.json({ ok: false, error: 'workdir, agent, sessionId, and objective are required' }, 400);
|
|
705
|
+
}
|
|
706
|
+
const bot = runtime.getBotRef();
|
|
707
|
+
if (!bot)
|
|
708
|
+
return c.json({ ok: false, error: 'bot not attached' }, 503);
|
|
709
|
+
const goal = await bot.setSessionGoal(workdir, agent, sessionId, {
|
|
710
|
+
objective,
|
|
711
|
+
tokenBudget: typeof tokenBudget === 'number' ? tokenBudget : null,
|
|
712
|
+
modelId: typeof modelId === 'string' ? modelId : undefined,
|
|
713
|
+
thinkingEffort: typeof thinkingEffort === 'string' ? thinkingEffort : undefined,
|
|
714
|
+
});
|
|
715
|
+
return c.json({ ok: true, goal });
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
app.post('/api/session-hub/session/goal/pause', async (c) => {
|
|
722
|
+
try {
|
|
723
|
+
const body = await c.req.json();
|
|
724
|
+
const { workdir, agent, sessionId } = body || {};
|
|
725
|
+
if (!workdir || !agent || !sessionId) {
|
|
726
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
727
|
+
}
|
|
728
|
+
const bot = runtime.getBotRef();
|
|
729
|
+
if (!bot)
|
|
730
|
+
return c.json({ ok: false, error: 'bot not attached' }, 503);
|
|
731
|
+
const goal = await bot.pauseSessionGoal(workdir, agent, sessionId);
|
|
732
|
+
return c.json({ ok: true, goal });
|
|
733
|
+
}
|
|
734
|
+
catch (e) {
|
|
735
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
app.post('/api/session-hub/session/goal/resume', async (c) => {
|
|
739
|
+
try {
|
|
740
|
+
const body = await c.req.json();
|
|
741
|
+
const { workdir, agent, sessionId, modelId, thinkingEffort } = body || {};
|
|
742
|
+
if (!workdir || !agent || !sessionId) {
|
|
743
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
744
|
+
}
|
|
745
|
+
const bot = runtime.getBotRef();
|
|
746
|
+
if (!bot)
|
|
747
|
+
return c.json({ ok: false, error: 'bot not attached' }, 503);
|
|
748
|
+
const goal = await bot.resumeSessionGoal(workdir, agent, sessionId, {
|
|
749
|
+
modelId: typeof modelId === 'string' ? modelId : undefined,
|
|
750
|
+
thinkingEffort: typeof thinkingEffort === 'string' ? thinkingEffort : undefined,
|
|
751
|
+
});
|
|
752
|
+
return c.json({ ok: true, goal });
|
|
753
|
+
}
|
|
754
|
+
catch (e) {
|
|
755
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
app.post('/api/session-hub/session/goal/clear', async (c) => {
|
|
759
|
+
try {
|
|
760
|
+
const body = await c.req.json();
|
|
761
|
+
const { workdir, agent, sessionId } = body || {};
|
|
762
|
+
if (!workdir || !agent || !sessionId) {
|
|
763
|
+
return c.json({ ok: false, error: 'workdir, agent, and sessionId are required' }, 400);
|
|
764
|
+
}
|
|
765
|
+
const bot = runtime.getBotRef();
|
|
766
|
+
if (!bot)
|
|
767
|
+
return c.json({ ok: false, error: 'bot not attached' }, 503);
|
|
768
|
+
await bot.clearSessionGoal(workdir, agent, sessionId);
|
|
769
|
+
return c.json({ ok: true });
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
// ==========================================================================
|
|
776
|
+
// Interaction prompts (human-in-the-loop)
|
|
777
|
+
// ==========================================================================
|
|
778
|
+
/** GET /api/interaction/:promptId — Get interaction prompt state. */
|
|
779
|
+
app.get('/api/interaction/:promptId', (c) => {
|
|
780
|
+
const { promptId } = c.req.param();
|
|
781
|
+
const result = getInteractionPrompt(promptId);
|
|
782
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
783
|
+
});
|
|
784
|
+
/** POST /api/interaction/:promptId/select — Select an option. */
|
|
785
|
+
app.post('/api/interaction/:promptId/select', async (c) => {
|
|
786
|
+
try {
|
|
787
|
+
const { promptId } = c.req.param();
|
|
788
|
+
const body = await c.req.json();
|
|
789
|
+
const { value, requestFreeform } = body || {};
|
|
790
|
+
if (!value && !requestFreeform) {
|
|
791
|
+
return c.json({ ok: false, error: 'value is required' }, 400);
|
|
792
|
+
}
|
|
793
|
+
const result = interactionSelectOption(promptId, value || '__other__', { requestFreeform: !!requestFreeform });
|
|
794
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
795
|
+
}
|
|
796
|
+
catch (e) {
|
|
797
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
/** POST /api/interaction/:promptId/text — Submit freeform text. */
|
|
801
|
+
app.post('/api/interaction/:promptId/text', async (c) => {
|
|
802
|
+
try {
|
|
803
|
+
const { promptId } = c.req.param();
|
|
804
|
+
const body = await c.req.json();
|
|
805
|
+
const { text } = body || {};
|
|
806
|
+
if (typeof text !== 'string') {
|
|
807
|
+
return c.json({ ok: false, error: 'text is required' }, 400);
|
|
808
|
+
}
|
|
809
|
+
const result = interactionSubmitText(promptId, text);
|
|
810
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
/** POST /api/interaction/:promptId/skip — Skip current question. */
|
|
817
|
+
app.post('/api/interaction/:promptId/skip', async (c) => {
|
|
818
|
+
try {
|
|
819
|
+
const { promptId } = c.req.param();
|
|
820
|
+
const result = interactionSkip(promptId);
|
|
821
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
822
|
+
}
|
|
823
|
+
catch (e) {
|
|
824
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
/** POST /api/interaction/:promptId/cancel — Cancel interaction prompt. */
|
|
828
|
+
app.post('/api/interaction/:promptId/cancel', async (c) => {
|
|
829
|
+
try {
|
|
830
|
+
const { promptId } = c.req.param();
|
|
831
|
+
const result = interactionCancel(promptId);
|
|
832
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
export default app;
|