pikiloom 0.4.26 → 0.4.27
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
- package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
- package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
- package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard API routes: session CRUD, workspace, streaming state.
|
|
3
|
-
*/
|
|
4
1
|
import { Hono } from 'hono';
|
|
5
2
|
import fs from 'node:fs';
|
|
6
3
|
import os from 'node:os';
|
|
@@ -15,9 +12,6 @@ import { cancelSessionTask, stopSessionTasks, getSessionStreamState, queueDashbo
|
|
|
15
12
|
import { querySessions, querySessionTail, querySessionMessages, getWorkspaceOverviews, updateSession, linkSessions, buildMigrationContext, exportSession, importSession, deleteSession, loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, } from '../../bot/session-hub.js';
|
|
16
13
|
import { DASHBOARD_PAGINATION } from '../../core/constants.js';
|
|
17
14
|
import { runtime } from '../runtime.js';
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Helpers
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
15
|
const DEFAULT_SESSION_PAGE_SIZE = DASHBOARD_PAGINATION.defaultPageSize;
|
|
22
16
|
const MAX_SESSION_PAGE_SIZE = DASHBOARD_PAGINATION.maxPageSize;
|
|
23
17
|
function parsePageNumber(value, fallback = 0) {
|
|
@@ -50,9 +44,6 @@ function enrichWithRuntimeStatus(sessions, bot) {
|
|
|
50
44
|
return sessions.map(session => {
|
|
51
45
|
const status = bot ? getSessionStatusForBot(bot, session) : null;
|
|
52
46
|
const isRunning = status ? status.isRunning : !!session.running;
|
|
53
|
-
// "Waiting on background work" only applies to a session that isn't
|
|
54
|
-
// currently running — surface the marker the agent parked (if any) so the
|
|
55
|
-
// dashboard can show a distinct "waiting" state instead of "completed".
|
|
56
47
|
const awaiting = !isRunning && session.workdir && session.sessionId
|
|
57
48
|
? readAwaitResume(session.workdir, session.agent, session.sessionId)
|
|
58
49
|
: null;
|
|
@@ -65,20 +56,12 @@ function enrichWithRuntimeStatus(sessions, bot) {
|
|
|
65
56
|
};
|
|
66
57
|
});
|
|
67
58
|
}
|
|
68
|
-
// Session list cards render only the *head* of these text fields (previews via
|
|
69
|
-
// firstMeaningfulLine / slice / sanitize) and use them for client-side substring
|
|
70
|
-
// search. A session whose last turn dumped a huge tool output or long answer would
|
|
71
|
-
// otherwise ship tens of KB per card that the list never displays — on a busy
|
|
72
|
-
// workspace the swim-lane ballooned to ~600KB, dominated by these fields. Cap each
|
|
73
|
-
// to a preview length: previews are unchanged and search still matches the head.
|
|
74
|
-
// Full text remains available from the session-detail / messages endpoints.
|
|
75
59
|
const LIST_PREVIEW_FIELD_CAP = 2048;
|
|
76
60
|
function capPreviewField(value) {
|
|
77
61
|
return typeof value === 'string' && value.length > LIST_PREVIEW_FIELD_CAP
|
|
78
62
|
? value.slice(0, LIST_PREVIEW_FIELD_CAP)
|
|
79
63
|
: value;
|
|
80
64
|
}
|
|
81
|
-
/** Thin a session for list/swim-lane responses by capping its heavy preview text. */
|
|
82
65
|
export function projectSessionForList(session) {
|
|
83
66
|
return {
|
|
84
67
|
...session,
|
|
@@ -176,14 +159,7 @@ async function parseSessionSendRequest(c) {
|
|
|
176
159
|
cleanup: async () => { },
|
|
177
160
|
};
|
|
178
161
|
}
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// Routes
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
162
|
const app = new Hono();
|
|
183
|
-
// ==========================================================================
|
|
184
|
-
// Legacy session routes (backward-compat for dashboard-ui)
|
|
185
|
-
// ==========================================================================
|
|
186
|
-
// Sessions per agent: GET /api/sessions/:agent
|
|
187
163
|
app.get('/api/sessions/:agent', async (c) => {
|
|
188
164
|
const agent = c.req.param('agent');
|
|
189
165
|
const config = loadUserConfig();
|
|
@@ -205,7 +181,6 @@ app.get('/api/sessions/:agent', async (c) => {
|
|
|
205
181
|
...paged,
|
|
206
182
|
});
|
|
207
183
|
});
|
|
208
|
-
// All sessions (swim lane): GET /api/sessions
|
|
209
184
|
app.get('/api/sessions', async (c) => {
|
|
210
185
|
const config = loadUserConfig();
|
|
211
186
|
const workdir = runtime.getRequestWorkdir(config);
|
|
@@ -231,7 +206,6 @@ app.get('/api/sessions', async (c) => {
|
|
|
231
206
|
}));
|
|
232
207
|
return c.json(swimLane);
|
|
233
208
|
});
|
|
234
|
-
// Session detail (tail): GET /api/session-detail/:agent/:id
|
|
235
209
|
app.get('/api/session-detail/:agent/:id', async (c) => {
|
|
236
210
|
const agent = c.req.param('agent');
|
|
237
211
|
const sessionId = decodeURIComponent(c.req.param('id'));
|
|
@@ -245,12 +219,8 @@ app.get('/api/session-detail/:agent/:id', async (c) => {
|
|
|
245
219
|
`messages=${tail.messages.length} error=${tail.error || '(none)'}`);
|
|
246
220
|
return c.json(tail);
|
|
247
221
|
});
|
|
248
|
-
// ==========================================================================
|
|
249
|
-
// Workspace CRUD
|
|
250
|
-
// ==========================================================================
|
|
251
222
|
app.get('/api/workspaces', (c) => {
|
|
252
223
|
const workspaces = loadWorkspaces();
|
|
253
|
-
// Always include the current runtimeWorkdir, deduplicating by path
|
|
254
224
|
const config = loadUserConfig();
|
|
255
225
|
const rwd = runtime.getRuntimeWorkdir(config);
|
|
256
226
|
if (rwd && !workspaces.some(w => w.path === rwd)) {
|
|
@@ -302,9 +272,6 @@ app.patch('/api/workspaces', async (c) => {
|
|
|
302
272
|
return c.json({ ok: false, error: e.message }, 500);
|
|
303
273
|
}
|
|
304
274
|
});
|
|
305
|
-
// ==========================================================================
|
|
306
|
-
// Workspace overviews
|
|
307
|
-
// ==========================================================================
|
|
308
275
|
app.get('/api/workspace-overviews', async (c) => {
|
|
309
276
|
try {
|
|
310
277
|
const overviews = await getWorkspaceOverviews();
|
|
@@ -314,9 +281,6 @@ app.get('/api/workspace-overviews', async (c) => {
|
|
|
314
281
|
return c.json({ ok: false, error: e.message }, 500);
|
|
315
282
|
}
|
|
316
283
|
});
|
|
317
|
-
// ==========================================================================
|
|
318
|
-
// Session hub operations
|
|
319
|
-
// ==========================================================================
|
|
320
284
|
app.post('/api/session-hub/sessions', async (c) => {
|
|
321
285
|
try {
|
|
322
286
|
const body = await c.req.json();
|
|
@@ -432,17 +396,7 @@ app.post('/api/session-hub/session/messages', async (c) => {
|
|
|
432
396
|
return c.json({ ok: false, error: e.message }, 500);
|
|
433
397
|
}
|
|
434
398
|
});
|
|
435
|
-
// Prepare a session message read for the dashboard:
|
|
436
|
-
// 1. Rewrite on-disk image/file blocks into compact attachment HTTP URLs so a
|
|
437
|
-
// remote browser can fetch the bytes (inline data: images pass through).
|
|
438
|
-
// 2. Append the session's delivered artifacts (files the agent handed the
|
|
439
|
-
// user via `im_send_file`) as a trailing assistant message, so they render
|
|
440
|
-
// and stay retrievable after a reload regardless of which terminal
|
|
441
|
-
// delivered them. Only added when this window includes the conversation
|
|
442
|
-
// tail (`!hasNewer`) to avoid duplicating across paginated reads.
|
|
443
399
|
function prepareSessionMessagesForDashboard(result, agent, sessionId) {
|
|
444
|
-
// Only operate in rich mode — a `rich:false` read returns plain text and must
|
|
445
|
-
// not gain a synthetic richMessages array.
|
|
446
400
|
if (result.richMessages === undefined)
|
|
447
401
|
return result;
|
|
448
402
|
const richMessages = result.richMessages.map(message => ({
|
|
@@ -461,7 +415,6 @@ function prepareSessionMessagesForDashboard(result, agent, sessionId) {
|
|
|
461
415
|
return result;
|
|
462
416
|
return { ...result, richMessages };
|
|
463
417
|
}
|
|
464
|
-
/** Plain-text fallback for the delivered-artifacts message (IM tail / exports). */
|
|
465
418
|
function deliveredSummaryText(blocks) {
|
|
466
419
|
const names = blocks
|
|
467
420
|
.map(b => b.fileName || (b.type === 'image' ? 'image' : 'file'))
|
|
@@ -470,12 +423,6 @@ function deliveredSummaryText(blocks) {
|
|
|
470
423
|
return '';
|
|
471
424
|
return names.length === 1 ? `Delivered: ${names[0]}` : `Delivered ${names.length} files: ${names.join(', ')}`;
|
|
472
425
|
}
|
|
473
|
-
// Attachment endpoint — serves on-disk images AND delivered files referenced by
|
|
474
|
-
// RichMessage image/file blocks via opaque base64url path tokens. The allowlist
|
|
475
|
-
// (see images.ts) confines reads to a known set of agent-managed dirs + the
|
|
476
|
-
// session's workdir + the per-session delivered-artifacts dir. Streams the
|
|
477
|
-
// bytes (no full-buffer) and supports Range so large artifacts (video,
|
|
478
|
-
// archives) download/seek without pinning memory.
|
|
479
426
|
app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
480
427
|
const agent = c.req.param('agent');
|
|
481
428
|
const sessionId = decodeURIComponent(c.req.param('id'));
|
|
@@ -492,16 +439,6 @@ app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
|
492
439
|
if (!requestedPath || requestedPath.includes('\0')) {
|
|
493
440
|
return c.json({ ok: false, error: 'invalid path' }, 400);
|
|
494
441
|
}
|
|
495
|
-
// Widen the allowlist with the session's recorded workdir when known —
|
|
496
|
-
// images generated under the project tree resolve cleanly. Session indexes
|
|
497
|
-
// are per-workdir and this URL carries no workdir, so a lookup against the
|
|
498
|
-
// runtime workdir alone misses sessions living in any OTHER registered
|
|
499
|
-
// workspace (the Session Hub renders all of them through this endpoint) —
|
|
500
|
-
// their user-attached images 403'd as "broken image" in the dashboard.
|
|
501
|
-
// Registered workspace roots come from server-side config, never request
|
|
502
|
-
// input, so widening to all of them keeps the same trust boundary and also
|
|
503
|
-
// covers the pending→native id promotion window where no index has the
|
|
504
|
-
// session yet.
|
|
505
442
|
const config = loadUserConfig();
|
|
506
443
|
const fallbackWorkdir = runtime.getRequestWorkdir(config);
|
|
507
444
|
const managed = findPikiloomSession(fallbackWorkdir, agent, sessionId);
|
|
@@ -524,14 +461,9 @@ app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
|
524
461
|
return c.json({ ok: false, error: 'not a file' }, 400);
|
|
525
462
|
const mime = mimeForArtifact(resolved);
|
|
526
463
|
const downloadName = sanitizeDownloadName(c.req.query('n'), resolved);
|
|
527
|
-
// Inline images so <img> renders them; everything else downloads with its
|
|
528
|
-
// pristine name. RFC 5987 filename* carries non-ASCII names safely.
|
|
529
464
|
const disposition = mime.startsWith('image/') ? 'inline' : 'attachment';
|
|
530
465
|
const asciiName = downloadName.replace(/[^\x20-\x7e]/g, '_').replace(/"/g, "'");
|
|
531
466
|
const contentDisposition = `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(downloadName)}`;
|
|
532
|
-
// The path is stamp-unique for delivered artifacts and hash-immutable for
|
|
533
|
-
// agent-managed dirs (`ig_<sha>.png`, …); the session lifecycle keeps the
|
|
534
|
-
// file stable — long cache is safe.
|
|
535
467
|
const baseHeaders = {
|
|
536
468
|
'Content-Type': mime,
|
|
537
469
|
'Content-Disposition': contentDisposition,
|
|
@@ -539,7 +471,6 @@ app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
|
539
471
|
'X-Content-Type-Options': 'nosniff',
|
|
540
472
|
'Accept-Ranges': 'bytes',
|
|
541
473
|
};
|
|
542
|
-
// Honor a single byte-range request (e.g. video seek / resumable download).
|
|
543
474
|
const range = parseByteRange(c.req.header('range'), stat.size);
|
|
544
475
|
if (range) {
|
|
545
476
|
const nodeStream = fs.createReadStream(resolved, { start: range.start, end: range.end });
|
|
@@ -555,16 +486,11 @@ app.get('/api/sessions/:agent/:id/attachment', async (c) => {
|
|
|
555
486
|
'Content-Length': String(stat.size),
|
|
556
487
|
});
|
|
557
488
|
});
|
|
558
|
-
/** Sanitize the optional `&n=` download-name hint; fall back to the basename. */
|
|
559
489
|
function sanitizeDownloadName(raw, resolved) {
|
|
560
490
|
const candidate = (raw || '').trim();
|
|
561
491
|
const name = candidate || path.basename(resolved);
|
|
562
|
-
// Strip path separators / control chars; the on-disk path is already
|
|
563
|
-
// validated — `n` only affects the Content-Disposition filename.
|
|
564
492
|
return name.replace(/[/\\\0\r\n]+/g, '_').replace(/^\.+/, '').slice(0, 200) || 'download';
|
|
565
493
|
}
|
|
566
|
-
/** Parse a single `bytes=start-end` range header against `size`; null if absent
|
|
567
|
-
* or unsatisfiable (caller then serves the full body). */
|
|
568
494
|
function parseByteRange(header, size) {
|
|
569
495
|
if (!header || size <= 0)
|
|
570
496
|
return null;
|
|
@@ -580,7 +506,6 @@ function parseByteRange(header, size) {
|
|
|
580
506
|
end = hasEnd ? parseInt(m[2], 10) : size - 1;
|
|
581
507
|
}
|
|
582
508
|
else if (hasEnd) {
|
|
583
|
-
// Suffix range: last N bytes.
|
|
584
509
|
const suffix = parseInt(m[2], 10);
|
|
585
510
|
if (suffix <= 0)
|
|
586
511
|
return null;
|
|
@@ -649,9 +574,6 @@ app.post('/api/session-hub/import', async (c) => {
|
|
|
649
574
|
return c.json({ ok: false, error: e.message }, 500);
|
|
650
575
|
}
|
|
651
576
|
});
|
|
652
|
-
// ==========================================================================
|
|
653
|
-
// Skills
|
|
654
|
-
// ==========================================================================
|
|
655
577
|
app.get('/api/session-hub/skills', (c) => {
|
|
656
578
|
const workdir = c.req.query('workdir') || '';
|
|
657
579
|
if (!workdir)
|
|
@@ -664,9 +586,6 @@ app.get('/api/session-hub/skills', (c) => {
|
|
|
664
586
|
return c.json({ ok: false, skills: [], error: e.message }, 500);
|
|
665
587
|
}
|
|
666
588
|
});
|
|
667
|
-
// ==========================================================================
|
|
668
|
-
// Session interaction (send / recall / steer / stream)
|
|
669
|
-
// ==========================================================================
|
|
670
589
|
app.post('/api/session-hub/session/send', async (c) => {
|
|
671
590
|
try {
|
|
672
591
|
const { workdir, agent, sessionId, prompt, model, effort, workflow, attachments, previousAgent, previousSessionId, cleanup } = await parseSessionSendRequest(c);
|
|
@@ -695,7 +614,6 @@ app.post('/api/session-hub/session/send', async (c) => {
|
|
|
695
614
|
return c.json({ ok: false, error: e.message }, 500);
|
|
696
615
|
}
|
|
697
616
|
});
|
|
698
|
-
// Polling endpoint: GET /api/session-hub/session/stream-state?agent=X&sessionId=Y
|
|
699
617
|
app.get('/api/session-hub/session/stream-state', (c) => {
|
|
700
618
|
const agent = c.req.query('agent') || '';
|
|
701
619
|
const sessionId = c.req.query('sessionId') || '';
|
|
@@ -704,9 +622,6 @@ app.get('/api/session-hub/session/stream-state', (c) => {
|
|
|
704
622
|
}
|
|
705
623
|
return c.json(getSessionStreamState(agent, sessionId));
|
|
706
624
|
});
|
|
707
|
-
// Fork: branch off a parent session at `atTurn`, queue the new prompt against
|
|
708
|
-
// the freshly forked child. Returns the queued task + the pending child session
|
|
709
|
-
// key so the dashboard can navigate the user into the child immediately.
|
|
710
625
|
app.post('/api/session-hub/session/fork', async (c) => {
|
|
711
626
|
try {
|
|
712
627
|
const body = await c.req.json();
|
|
@@ -749,10 +664,6 @@ app.post('/api/session-hub/session/recall', async (c) => {
|
|
|
749
664
|
return c.json({ ok: false, error: e.message }, 500);
|
|
750
665
|
}
|
|
751
666
|
});
|
|
752
|
-
// Stop only the currently running stream for a session — queued follow-ups
|
|
753
|
-
// are kept so they run next. Takes (agent, sessionId) rather than taskId so it
|
|
754
|
-
// works during the moment after a fresh send where the client hasn't yet
|
|
755
|
-
// learned the streamTaskId.
|
|
756
667
|
app.post('/api/session-hub/session/stop', async (c) => {
|
|
757
668
|
try {
|
|
758
669
|
const body = await c.req.json();
|
|
@@ -781,9 +692,6 @@ app.post('/api/session-hub/session/steer', async (c) => {
|
|
|
781
692
|
return c.json({ ok: false, error: e.message }, 500);
|
|
782
693
|
}
|
|
783
694
|
});
|
|
784
|
-
// ==========================================================================
|
|
785
|
-
// Persistent thread goal (analogous to Codex CLI's `/goal`).
|
|
786
|
-
// ==========================================================================
|
|
787
695
|
app.get('/api/session-hub/session/goal', async (c) => {
|
|
788
696
|
const workdir = c.req.query('workdir') || '';
|
|
789
697
|
const agent = c.req.query('agent') || '';
|
|
@@ -878,16 +786,11 @@ app.post('/api/session-hub/session/goal/clear', async (c) => {
|
|
|
878
786
|
return c.json({ ok: false, error: e.message }, 500);
|
|
879
787
|
}
|
|
880
788
|
});
|
|
881
|
-
// ==========================================================================
|
|
882
|
-
// Interaction prompts (human-in-the-loop)
|
|
883
|
-
// ==========================================================================
|
|
884
|
-
/** GET /api/interaction/:promptId — Get interaction prompt state. */
|
|
885
789
|
app.get('/api/interaction/:promptId', (c) => {
|
|
886
790
|
const { promptId } = c.req.param();
|
|
887
791
|
const result = getInteractionPrompt(promptId);
|
|
888
792
|
return c.json(result, result.ok ? 200 : 503);
|
|
889
793
|
});
|
|
890
|
-
/** POST /api/interaction/:promptId/select — Select an option. */
|
|
891
794
|
app.post('/api/interaction/:promptId/select', async (c) => {
|
|
892
795
|
try {
|
|
893
796
|
const { promptId } = c.req.param();
|
|
@@ -903,7 +806,6 @@ app.post('/api/interaction/:promptId/select', async (c) => {
|
|
|
903
806
|
return c.json({ ok: false, error: e.message }, 500);
|
|
904
807
|
}
|
|
905
808
|
});
|
|
906
|
-
/** POST /api/interaction/:promptId/text — Submit freeform text. */
|
|
907
809
|
app.post('/api/interaction/:promptId/text', async (c) => {
|
|
908
810
|
try {
|
|
909
811
|
const { promptId } = c.req.param();
|
|
@@ -919,7 +821,6 @@ app.post('/api/interaction/:promptId/text', async (c) => {
|
|
|
919
821
|
return c.json({ ok: false, error: e.message }, 500);
|
|
920
822
|
}
|
|
921
823
|
});
|
|
922
|
-
/** POST /api/interaction/:promptId/skip — Skip current question. */
|
|
923
824
|
app.post('/api/interaction/:promptId/skip', async (c) => {
|
|
924
825
|
try {
|
|
925
826
|
const { promptId } = c.req.param();
|
|
@@ -930,7 +831,6 @@ app.post('/api/interaction/:promptId/skip', async (c) => {
|
|
|
930
831
|
return c.json({ ok: false, error: e.message }, 500);
|
|
931
832
|
}
|
|
932
833
|
});
|
|
933
|
-
/** POST /api/interaction/:promptId/cancel — Cancel interaction prompt. */
|
|
934
834
|
app.post('/api/interaction/:promptId/cancel', async (c) => {
|
|
935
835
|
try {
|
|
936
836
|
const { promptId } = c.req.param();
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* runtime.ts — Singleton module holding the runtime state previously
|
|
3
|
-
* scattered across startDashboard() closure variables.
|
|
4
|
-
*
|
|
5
|
-
* Provides bot ref management, runtime prefs (agent/model/effort),
|
|
6
|
-
* channel state caching, and validated setup state construction.
|
|
7
|
-
*/
|
|
8
1
|
import { EventEmitter } from 'node:events';
|
|
9
2
|
import { applyChannelEnvFallback, loadUserConfig, resolveUserWorkdir } from '../core/config/user-config.js';
|
|
10
3
|
import { listAgents, resolveDefaultAgent } from '../agent/index.js';
|
|
@@ -15,18 +8,9 @@ import { DASHBOARD_TIMEOUTS } from '../core/constants.js';
|
|
|
15
8
|
import { withTimeoutFallback } from '../core/utils.js';
|
|
16
9
|
import { writeScopedLog } from '../core/logging.js';
|
|
17
10
|
import { DEFAULT_AGENT_EFFORTS, DEFAULT_AGENT_MODELS, resolveAgentEffort, resolveAgentModel, resolveAgentWorkflowEnabled, resolveClaudeAccessMode, setAgentEffortEnv, setAgentModelEnv, setAgentWorkflowEnv, setClaudeAccessModeEnv, } from '../core/config/runtime-config.js';
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Constants
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
11
|
const CHANNEL_STATUS_VALIDATION_TIMEOUT_MS = DASHBOARD_TIMEOUTS.channelStatusValidation;
|
|
22
12
|
const CHANNEL_STATUS_CACHE_TTL_MS = DASHBOARD_TIMEOUTS.channelStatusCacheTtl;
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
13
|
function buildLocalChannelStates(rawConfig) {
|
|
27
|
-
// Hydrate env-only channel tokens (docker / systemd) so the dashboard
|
|
28
|
-
// doesn't report "missing" when the operator passed `-e TELEGRAM_BOT_TOKEN=…`
|
|
29
|
-
// instead of editing setting.json.
|
|
30
14
|
const config = applyChannelEnvFallback(rawConfig);
|
|
31
15
|
const weixinBaseUrl = String(config.weixinBaseUrl || '').trim();
|
|
32
16
|
const weixinBotToken = String(config.weixinBotToken || '').trim();
|
|
@@ -131,30 +115,17 @@ function buildLocalChannelStates(rawConfig) {
|
|
|
131
115
|
},
|
|
132
116
|
];
|
|
133
117
|
}
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
// Runtime singleton
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
118
|
class Runtime {
|
|
138
119
|
botRef = null;
|
|
139
120
|
runtimePrefs = { models: {}, efforts: {}, workflow: {}, accessMode: {} };
|
|
140
|
-
/** Dashboard event bus — WebSocket connections subscribe to this. */
|
|
141
121
|
events = new EventEmitter();
|
|
142
|
-
/** Emit a dashboard event to all connected WebSocket clients. */
|
|
143
122
|
emitDashboardEvent(event) {
|
|
144
123
|
this.events.emit('dashboard-event', event);
|
|
145
124
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Per-channel validation cache. Keyed by channel name so that changing
|
|
148
|
-
* one channel's credentials (or the channels array) doesn't invalidate
|
|
149
|
-
* the cached validation result for unrelated channels — which would
|
|
150
|
-
* otherwise flicker their UI status to "Validating…" until the next
|
|
151
|
-
* poll completes.
|
|
152
|
-
*/
|
|
153
125
|
channelStateCache = new Map();
|
|
154
126
|
knownAgents = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
155
127
|
defaultModels = DEFAULT_AGENT_MODELS;
|
|
156
128
|
defaultEfforts = DEFAULT_AGENT_EFFORTS;
|
|
157
|
-
// -- Bot ref management --
|
|
158
129
|
getBotRef() {
|
|
159
130
|
return this.botRef;
|
|
160
131
|
}
|
|
@@ -178,40 +149,31 @@ class Runtime {
|
|
|
178
149
|
if (agent === 'claude' && (mode === 'subscription' || mode === 'api'))
|
|
179
150
|
bot.setClaudeAccessMode(mode);
|
|
180
151
|
}
|
|
181
|
-
// Wire stream snapshots → dashboard WebSocket
|
|
182
152
|
const prevPhases = new Map();
|
|
183
153
|
bot.onStreamSnapshot((sessionKey, snapshot) => {
|
|
184
154
|
this.emitDashboardEvent({ type: 'stream-update', key: sessionKey, snapshot });
|
|
185
|
-
// Emit sessions-changed on phase *transitions* (not every snapshot update)
|
|
186
|
-
// so the sidebar refreshes when a session starts running, finishes, etc.
|
|
187
155
|
const phase = snapshot && typeof snapshot === 'object' ? snapshot.phase : null;
|
|
188
156
|
const prev = prevPhases.get(sessionKey) ?? null;
|
|
189
157
|
if (phase !== prev) {
|
|
190
158
|
prevPhases.set(sessionKey, phase);
|
|
191
159
|
if (!phase)
|
|
192
|
-
prevPhases.delete(sessionKey);
|
|
160
|
+
prevPhases.delete(sessionKey);
|
|
193
161
|
this.emitDashboardEvent({ type: 'sessions-changed', key: sessionKey });
|
|
194
162
|
}
|
|
195
163
|
});
|
|
196
164
|
}
|
|
197
|
-
// -- Type guards --
|
|
198
165
|
isAgent(value) {
|
|
199
166
|
return typeof value === 'string' && this.knownAgents.has(value);
|
|
200
167
|
}
|
|
201
|
-
// -- Workdir --
|
|
202
168
|
getRuntimeWorkdir(config) {
|
|
203
169
|
return this.botRef?.workdir || resolveUserWorkdir({ config });
|
|
204
170
|
}
|
|
205
171
|
getRequestWorkdir(config = loadUserConfig()) {
|
|
206
172
|
return this.getRuntimeWorkdir(config);
|
|
207
173
|
}
|
|
208
|
-
// -- Agent / model / effort --
|
|
209
174
|
getRuntimeDefaultAgent(config) {
|
|
210
175
|
if (this.botRef)
|
|
211
176
|
return this.botRef.defaultAgent;
|
|
212
|
-
// No bot yet (e.g. setup flow): resolve the stored preference (baseline
|
|
213
|
-
// 'codex' when unset) against installed CLIs so the dashboard never
|
|
214
|
-
// surfaces an uninstalled default the user can't run.
|
|
215
177
|
const preferred = this.runtimePrefs.defaultAgent || config.defaultAgent || 'codex';
|
|
216
178
|
return resolveDefaultAgent(preferred, listAgents().agents);
|
|
217
179
|
}
|
|
@@ -254,7 +216,6 @@ class Runtime {
|
|
|
254
216
|
return pref;
|
|
255
217
|
return resolveClaudeAccessMode(config);
|
|
256
218
|
}
|
|
257
|
-
// -- Channel state cache --
|
|
258
219
|
credKeyForChannel(channel, config) {
|
|
259
220
|
switch (channel) {
|
|
260
221
|
case 'weixin':
|
|
@@ -295,13 +256,6 @@ class Runtime {
|
|
|
295
256
|
validateChannel(channel, config) {
|
|
296
257
|
switch (channel) {
|
|
297
258
|
case 'weixin':
|
|
298
|
-
// The Weixin getupdates endpoint is a long-poll that holds the
|
|
299
|
-
// connection open for ~8s by default. For dashboard validation we
|
|
300
|
-
// only care whether the credentials are accepted (bad creds return
|
|
301
|
-
// an errcode quickly; good creds simply hold the poll). Use a
|
|
302
|
-
// tight internal timeout so we resolve well within the dashboard's
|
|
303
|
-
// CHANNEL_STATUS_VALIDATION_TIMEOUT_MS window — the abort path
|
|
304
|
-
// returns ret=0 which is treated as "valid".
|
|
305
259
|
return validateWeixinConfig(config.weixinBaseUrl, config.weixinBotToken, config.weixinAccountId, { timeoutMs: 2_000 }).then(r => r.state);
|
|
306
260
|
case 'telegram':
|
|
307
261
|
return validateTelegramConfig(config.telegramBotToken, config.telegramAllowedChatIds).then(r => r.state);
|
|
@@ -321,7 +275,6 @@ class Runtime {
|
|
|
321
275
|
const config = applyChannelEnvFallback(rawConfig);
|
|
322
276
|
const now = Date.now();
|
|
323
277
|
const fallback = buildLocalChannelStates(config);
|
|
324
|
-
// Channels listed in the same order as buildLocalChannelStates().
|
|
325
278
|
const channelOrder = fallback.map(state => state.channel);
|
|
326
279
|
const plans = channelOrder.map((channel, idx) => {
|
|
327
280
|
const key = this.credKeyForChannel(channel, config);
|
|
@@ -336,10 +289,6 @@ class Runtime {
|
|
|
336
289
|
return Promise.resolve(plan.cached);
|
|
337
290
|
return withTimeoutFallback(plan.livePromise, CHANNEL_STATUS_VALIDATION_TIMEOUT_MS, plan.fallback);
|
|
338
291
|
}));
|
|
339
|
-
// Persist freshly-validated channels into the per-channel cache. If a
|
|
340
|
-
// channel's validation timed out, keep the live promise alive in the
|
|
341
|
-
// background so the next dashboard poll can pick up the real result
|
|
342
|
-
// instantly instead of re-issuing the network call.
|
|
343
292
|
plans.forEach((plan, i) => {
|
|
344
293
|
if (!plan.livePromise)
|
|
345
294
|
return;
|
|
@@ -355,7 +304,6 @@ class Runtime {
|
|
|
355
304
|
void plan.livePromise.then(bgState => {
|
|
356
305
|
if (!shouldCacheChannelStates([bgState]))
|
|
357
306
|
return;
|
|
358
|
-
// Skip if newer credentials have replaced the cache for this channel.
|
|
359
307
|
const current = this.channelStateCache.get(plan.channel);
|
|
360
308
|
if (current && current.key !== plan.key)
|
|
361
309
|
return;
|
|
@@ -368,7 +316,6 @@ class Runtime {
|
|
|
368
316
|
});
|
|
369
317
|
return resolved;
|
|
370
318
|
}
|
|
371
|
-
// -- Setup state --
|
|
372
319
|
getSetupState(config = loadUserConfig(), agentOptions = {}) {
|
|
373
320
|
const agents = listAgents(agentOptions).agents;
|
|
374
321
|
const channels = buildLocalChannelStates(applyChannelEnvFallback(config));
|
|
@@ -393,7 +340,6 @@ class Runtime {
|
|
|
393
340
|
channels,
|
|
394
341
|
});
|
|
395
342
|
}
|
|
396
|
-
// -- Logging --
|
|
397
343
|
log(message, level = 'info') {
|
|
398
344
|
writeScopedLog('dashboard', message, { level });
|
|
399
345
|
}
|
|
@@ -404,7 +350,4 @@ class Runtime {
|
|
|
404
350
|
this.log(message, 'warn');
|
|
405
351
|
}
|
|
406
352
|
}
|
|
407
|
-
// ---------------------------------------------------------------------------
|
|
408
|
-
// Singleton export
|
|
409
|
-
// ---------------------------------------------------------------------------
|
|
410
353
|
export const runtime = new Runtime();
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hono-based dashboard HTTP server: static files and API routes.
|
|
3
|
-
*/
|
|
4
1
|
import http from 'node:http';
|
|
5
2
|
import { Hono } from 'hono';
|
|
6
3
|
import { compress } from 'hono/compress';
|
|
@@ -19,30 +16,13 @@ import localModelsRoutes from './routes/local-models.js';
|
|
|
19
16
|
import { runtime } from './runtime.js';
|
|
20
17
|
import { registerProcessRuntime } from '../core/process-control.js';
|
|
21
18
|
import { mountPikichannel } from '../pikichannel/server.js';
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Constants
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
19
|
const DASHBOARD_PORT_RETRY_LIMIT = 10;
|
|
26
|
-
// The dashboard's live push transport is pikichannel (`/pikichannel/ws`) — the
|
|
27
|
-
// same universal channel mobile/web clients use. There is no separate dashboard
|
|
28
|
-
// WebSocket layer anymore; the pikichannel adapter subscribes to the runtime
|
|
29
|
-
// 'dashboard-event' bus directly, so the SPA and remote clients share one stack.
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Server
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
20
|
export async function startDashboard(opts = {}) {
|
|
34
21
|
const preferredPort = opts.port || 3939;
|
|
35
22
|
if (opts.bot)
|
|
36
23
|
runtime.attachBot(opts.bot);
|
|
37
24
|
const app = new Hono();
|
|
38
|
-
// -- Compression --
|
|
39
|
-
// gzip/deflate every compressible response (JSON API payloads, JS/CSS bundles,
|
|
40
|
-
// the HTML shell). Session message/list endpoints ship hundreds of KB of JSON;
|
|
41
|
-
// Vite chunks are another few hundred KB raw. The middleware skips already-
|
|
42
|
-
// compressed binary types (png/ico) by content-type, so the immutable image
|
|
43
|
-
// assets pay no CPU cost. Registered first so it wraps both routes and static.
|
|
44
25
|
app.use('*', compress());
|
|
45
|
-
// -- API routes --
|
|
46
26
|
app.route('/', configRoutes);
|
|
47
27
|
app.route('/', agentRoutes);
|
|
48
28
|
app.route('/', sessionRoutes);
|
|
@@ -50,10 +30,6 @@ export async function startDashboard(opts = {}) {
|
|
|
50
30
|
app.route('/', cliRoutes);
|
|
51
31
|
app.route('/', modelsRoutes);
|
|
52
32
|
app.route('/', localModelsRoutes);
|
|
53
|
-
// -- pikichannel: pluggable agent-session transport (WebSocket + WebRTC) --
|
|
54
|
-
// Mounted BEFORE the static catch-all so its routes (demo page, browser SDK,
|
|
55
|
-
// status) win. Returns a handle whose upgrade router is attached per HTTP
|
|
56
|
-
// server below. Failure here must not block the dashboard, so it is guarded.
|
|
57
33
|
let pikichannel = null;
|
|
58
34
|
try {
|
|
59
35
|
pikichannel = await mountPikichannel(app);
|
|
@@ -61,21 +37,13 @@ export async function startDashboard(opts = {}) {
|
|
|
61
37
|
catch (err) {
|
|
62
38
|
runtime.warn(`[pikichannel] mount failed: ${err?.message || err}`);
|
|
63
39
|
}
|
|
64
|
-
// -- Static files: serve dashboard build output --
|
|
65
|
-
// Resolve path relative to this file's location (src/ or dist/)
|
|
66
40
|
const dashboardRoot = path.resolve(import.meta.dirname, '..', '..', 'dashboard', 'dist');
|
|
67
|
-
// Serve /assets/* for Vite-hashed JS/CSS bundles. Filenames are
|
|
68
|
-
// content-hashed, so they can be cached indefinitely — a new build emits
|
|
69
|
-
// new filenames rather than mutating these.
|
|
70
41
|
app.use('/assets/*', serveStatic({
|
|
71
42
|
root: dashboardRoot,
|
|
72
43
|
onFound: (_path, c) => {
|
|
73
44
|
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
|
74
45
|
},
|
|
75
46
|
}));
|
|
76
|
-
// Serve other static files at root level (favicon, manifest, etc.).
|
|
77
|
-
// This mount also serves index.html for "/" (directory index), so the HTML
|
|
78
|
-
// shell is tagged no-cache here too — same reason as the SPA fallback below.
|
|
79
47
|
app.use('/*', serveStatic({
|
|
80
48
|
root: dashboardRoot,
|
|
81
49
|
onFound: (p, c) => {
|
|
@@ -83,21 +51,15 @@ export async function startDashboard(opts = {}) {
|
|
|
83
51
|
c.header('Cache-Control', 'no-cache');
|
|
84
52
|
},
|
|
85
53
|
onNotFound: () => {
|
|
86
|
-
// Fall through to the SPA catch-all below
|
|
87
54
|
},
|
|
88
55
|
}));
|
|
89
|
-
// SPA fallback: serve index.html for all non-API routes
|
|
90
56
|
app.get('*', async (c) => {
|
|
91
|
-
// Don't catch API routes that fell through (shouldn't happen, but guard anyway)
|
|
92
57
|
if (c.req.path.startsWith('/api/')) {
|
|
93
58
|
return c.json({ error: 'Not Found' }, 404);
|
|
94
59
|
}
|
|
95
60
|
const indexPath = path.join(dashboardRoot, 'index.html');
|
|
96
61
|
try {
|
|
97
62
|
const html = fs.readFileSync(indexPath, 'utf-8');
|
|
98
|
-
// The HTML shell references content-hashed asset filenames, so it must
|
|
99
|
-
// never be cached: otherwise an open tab keeps loading stale JS after the
|
|
100
|
-
// server self-updates (npx pikiloom@latest) until a manual hard refresh.
|
|
101
63
|
c.header('Cache-Control', 'no-cache');
|
|
102
64
|
return c.html(html);
|
|
103
65
|
}
|
|
@@ -105,7 +67,6 @@ export async function startDashboard(opts = {}) {
|
|
|
105
67
|
return c.text('Dashboard build not found. Run: npm run build:dashboard', 500);
|
|
106
68
|
}
|
|
107
69
|
});
|
|
108
|
-
// -- Process runtime registration --
|
|
109
70
|
let nodeServer = null;
|
|
110
71
|
const RESTART_CLOSE_TIMEOUT_MS = 3000;
|
|
111
72
|
const unregisterProcessRuntime = registerProcessRuntime({
|
|
@@ -115,14 +76,11 @@ export async function startDashboard(opts = {}) {
|
|
|
115
76
|
resolve();
|
|
116
77
|
return;
|
|
117
78
|
}
|
|
118
|
-
// Tear down pikichannel peers first — otherwise server.close() hangs
|
|
119
|
-
// waiting for the persistent WebSocket/datachannel connections to end.
|
|
120
79
|
pikichannel?.stop();
|
|
121
80
|
const timer = setTimeout(resolve, RESTART_CLOSE_TIMEOUT_MS);
|
|
122
81
|
nodeServer.close(() => { clearTimeout(timer); resolve(); });
|
|
123
82
|
}),
|
|
124
83
|
});
|
|
125
|
-
// -- Start server with port retry --
|
|
126
84
|
return new Promise((resolve, reject) => {
|
|
127
85
|
let nextPort = preferredPort;
|
|
128
86
|
let settled = false;
|
|
@@ -134,13 +92,8 @@ export async function startDashboard(opts = {}) {
|
|
|
134
92
|
};
|
|
135
93
|
function tryListen(port) {
|
|
136
94
|
try {
|
|
137
|
-
// Create HTTP server manually so we can attach WebSocket upgrade handler
|
|
138
|
-
// before Hono's request listener consumes the connection.
|
|
139
95
|
const requestListener = getRequestListener(app.fetch);
|
|
140
96
|
const server = http.createServer(requestListener);
|
|
141
|
-
// Single WebSocket-upgrade dispatcher (attached before listening so no
|
|
142
|
-
// upgrade is missed): pikichannel owns `/pikichannel/*` — its WS data
|
|
143
|
-
// channel and WebRTC signaling. Anything else is rejected.
|
|
144
97
|
server.on('upgrade', (req, socket, head) => {
|
|
145
98
|
if (pikichannel?.handleUpgrade(req, socket, head))
|
|
146
99
|
return;
|
|
@@ -156,7 +109,6 @@ export async function startDashboard(opts = {}) {
|
|
|
156
109
|
const dashUrl = `http://localhost:${actualPort}`;
|
|
157
110
|
const ts = new Date().toTimeString().slice(0, 8);
|
|
158
111
|
process.stdout.write(`[pikiloom ${ts}] dashboard: ${dashUrl}\n`);
|
|
159
|
-
// Preload agent status cache so the first dashboard page load is instant
|
|
160
112
|
preloadAgentStatus();
|
|
161
113
|
if (opts.open !== false) {
|
|
162
114
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
@@ -180,7 +132,6 @@ export async function startDashboard(opts = {}) {
|
|
|
180
132
|
},
|
|
181
133
|
});
|
|
182
134
|
});
|
|
183
|
-
// Handle EADDRINUSE by retrying on next port
|
|
184
135
|
server.on('error', (err) => {
|
|
185
136
|
if (settled)
|
|
186
137
|
return;
|