pikiloom 0.4.26 → 0.4.28

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.
Files changed (156) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
  10. package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
  11. package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
  12. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  13. package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
  153. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  154. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  155. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  156. 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); // clean up null entries
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();
@@ -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;