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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Shared rendering utilities used by channel-specific renderers.
3
+ *
4
+ * Contains types, pure-data helpers, and functions that are identical across platforms.
5
+ * Platform-specific formatting (HTML vs Markdown) stays in the respective render files.
6
+ */
7
+ import { materializeImage } from '../agent/index.js';
8
+ import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
9
+ import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './streaming.js';
10
+ import { supportsChannelCapability } from '../channels/base.js';
11
+ import { agentLog, agentWarn } from '../agent/index.js';
12
+ // ---------------------------------------------------------------------------
13
+ // GFM table parsing
14
+ // ---------------------------------------------------------------------------
15
+ /** Parse GFM table lines into structured headers + rows. */
16
+ export function parseGfmTable(tableLines) {
17
+ if (tableLines.length < 3)
18
+ return null;
19
+ const parseRow = (line) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
20
+ const isSep = (line) => {
21
+ const cells = parseRow(line);
22
+ return cells.length > 0 && cells.every(c => /^:?-{2,}:?$/.test(c));
23
+ };
24
+ let headerIdx = -1;
25
+ for (let i = 0; i < tableLines.length - 1; i++) {
26
+ if (isSep(tableLines[i + 1])) {
27
+ headerIdx = i;
28
+ break;
29
+ }
30
+ }
31
+ if (headerIdx < 0)
32
+ return null;
33
+ const headers = parseRow(tableLines[headerIdx]);
34
+ const rows = [];
35
+ for (let i = headerIdx + 2; i < tableLines.length; i++) {
36
+ if (isSep(tableLines[i]))
37
+ continue;
38
+ rows.push(parseRow(tableLines[i]));
39
+ }
40
+ return rows.length ? { headers, rows } : null;
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Footer helpers
44
+ // ---------------------------------------------------------------------------
45
+ export function fmtCompactUptime(ms) {
46
+ return fmtUptime(ms).replace(/\s+/g, '');
47
+ }
48
+ export function footerStatusSymbol(status) {
49
+ switch (status) {
50
+ case 'running': return '●';
51
+ case 'done': return '✓';
52
+ case 'failed': return '✗';
53
+ }
54
+ }
55
+ /**
56
+ * Drop a leading `provider/` segment from long model ids so the footer stays
57
+ * readable on narrow IM clients. `anthropic/claude-sonnet-4` → `claude-sonnet-4`,
58
+ * `deepseek/deepseek-v4-flash` → `deepseek-v4-flash`. Already-short ids are
59
+ * returned unchanged.
60
+ */
61
+ function compactModelLabel(model) {
62
+ const trimmed = model.trim();
63
+ if (trimmed.length <= 24)
64
+ return trimmed;
65
+ const slashIdx = trimmed.indexOf('/');
66
+ return slashIdx > 0 ? trimmed.slice(slashIdx + 1) : trimmed;
67
+ }
68
+ /**
69
+ * Split footer fields into a primary identity line (agent + model) and a
70
+ * secondary runtime line (effort, context%, elapsed). Channel renderers
71
+ * compose the two lines with their own visual styling so narrow IM clients
72
+ * never have to soft-wrap a single dense line.
73
+ */
74
+ export function formatFooterParts(agent, elapsedMs, meta, contextPercent, decorations) {
75
+ const identityParts = [agent];
76
+ if (decorations?.model)
77
+ identityParts.push(compactModelLabel(decorations.model));
78
+ const runtimeParts = [];
79
+ if (decorations?.effort)
80
+ runtimeParts.push(decorations.effort);
81
+ const ctx = contextPercent ?? meta?.contextPercent ?? null;
82
+ if (ctx != null)
83
+ runtimeParts.push(`${ctx}%`);
84
+ runtimeParts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
85
+ // BYOK attribution — tells the user the turn is being routed through a
86
+ // third-party provider rather than the agent CLI's native auth path.
87
+ // Tucked at the end of the runtime line so it doesn't crowd the (often
88
+ // long) identity line on narrow IM clients.
89
+ const providerName = meta?.providerName ?? decorations?.provider ?? null;
90
+ if (providerName)
91
+ runtimeParts.push(`via ${providerName}`);
92
+ return {
93
+ identity: identityParts.join(' · '),
94
+ runtime: runtimeParts.join(' · '),
95
+ };
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Activity trimming
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Trim the activity narrative for a streaming preview. Keeps the **most
102
+ * recent** lines that fit the budget — the user is watching the turn live,
103
+ * what just happened matters far more than what happened 30 tool-calls ago.
104
+ * A leading `...` marker signals "earlier activity dropped" when truncation
105
+ * happens; the tail order is preserved.
106
+ */
107
+ export function trimActivityForPreview(text, maxChars = 900) {
108
+ if (text.length <= maxChars)
109
+ return text;
110
+ const lines = text.split('\n').filter(line => line.trim());
111
+ if (lines.length <= 1) {
112
+ // Single very-long line — keep the trailing characters with a leading
113
+ // ellipsis so the freshest content is visible.
114
+ return '...' + text.slice(text.length - Math.max(0, maxChars - 3));
115
+ }
116
+ const ellipsis = '...';
117
+ const budget = Math.max(0, maxChars - ellipsis.length - 1);
118
+ const tail = [];
119
+ let used = 0;
120
+ for (let i = lines.length - 1; i >= 0; i--) {
121
+ const line = lines[i];
122
+ const extra = line.length + (tail.length ? 1 : 0);
123
+ if (used + extra > budget)
124
+ break;
125
+ tail.unshift(line);
126
+ used += extra;
127
+ }
128
+ if (!tail.length) {
129
+ return ellipsis + '\n' + lines[lines.length - 1].slice(-Math.max(0, maxChars - ellipsis.length - 1));
130
+ }
131
+ if (tail.length === lines.length)
132
+ return tail.join('\n');
133
+ return [ellipsis, ...tail].join('\n');
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Provider usage (plain-text builder — caller wraps as needed)
137
+ // ---------------------------------------------------------------------------
138
+ function rawUsageLine(parts) {
139
+ return parts.filter(part => !!part && String(part).trim()).join(' ');
140
+ }
141
+ export function buildProviderUsageLines(usage) {
142
+ const lines = [
143
+ { text: '', bold: false },
144
+ { text: 'Provider Usage', bold: true },
145
+ ];
146
+ if (!usage.ok) {
147
+ lines.push({ text: ` Unavailable: ${usage.error || 'No recent usage data found.'}` });
148
+ return lines;
149
+ }
150
+ if (usage.capturedAt) {
151
+ const capturedAtMs = Date.parse(usage.capturedAt);
152
+ if (Number.isFinite(capturedAtMs)) {
153
+ lines.push({ text: ` Updated: ${fmtUptime(Math.max(0, Date.now() - capturedAtMs))} ago` });
154
+ }
155
+ }
156
+ if (!usage.windows.length) {
157
+ lines.push({ text: ` ${usage.status ? `status=${usage.status}` : 'No window data'}` });
158
+ return lines;
159
+ }
160
+ for (const window of usage.windows) {
161
+ const details = rawUsageLine([
162
+ window.usedPercent != null ? `${window.usedPercent}% used` : null,
163
+ window.status ? `status=${window.status}` : null,
164
+ window.resetAfterSeconds != null ? `resetAfterSeconds=${window.resetAfterSeconds}` : null,
165
+ ]);
166
+ lines.push({ text: ` ${window.label}: ${details || 'No details'}` });
167
+ }
168
+ return lines;
169
+ }
170
+ /**
171
+ * Iterate an assistant turn's image MessageBlocks and dispatch each through
172
+ * the channel's `sendImage` capability. No-op when the channel doesn't claim
173
+ * `sendImage`. Errors per image are logged but don't block the rest of the
174
+ * dispatch — the text reply path is responsible for the user-visible summary.
175
+ *
176
+ * Returns the list of `{messageId, caption}` entries so the caller can register
177
+ * them with the session for "reply to continue" linkage.
178
+ */
179
+ export async function dispatchImageBlocks(channel, blocks, opts) {
180
+ if (!blocks?.length)
181
+ return [];
182
+ if (!supportsChannelCapability(channel, 'sendImage'))
183
+ return [];
184
+ const out = [];
185
+ let index = 0;
186
+ for (const block of blocks) {
187
+ if (block.type !== 'image')
188
+ continue;
189
+ index++;
190
+ const materialized = materializeImage(block);
191
+ if (!materialized) {
192
+ (opts.log || agentLog)(`[image-dispatch] skipped block #${index}: could not materialize bytes`);
193
+ continue;
194
+ }
195
+ try {
196
+ const messageId = await channel.sendImage(opts.chatId, materialized.bytes, {
197
+ mime: materialized.mime,
198
+ caption: materialized.caption,
199
+ replyTo: opts.replyTo,
200
+ messageThreadId: opts.messageThreadId,
201
+ });
202
+ out.push({ messageId, caption: materialized.caption });
203
+ }
204
+ catch (err) {
205
+ (opts.log || agentWarn)(`[image-dispatch] send failed #${index}: ${err?.message || err}`);
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+ export function extractFinalReplyData(agent, result) {
211
+ const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
212
+ const elapsedMs = result.elapsedS * 1000;
213
+ let activityNarrative = null;
214
+ let activityCommandSummary = null;
215
+ if (result.activity) {
216
+ const summary = parseActivitySummary(result.activity);
217
+ const narrative = summary.narrative.join('\n');
218
+ if (narrative) {
219
+ activityNarrative = narrative.length > 1600 ? '...\n' + narrative.slice(-1600) : narrative;
220
+ }
221
+ const cmdSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
222
+ if (cmdSummary)
223
+ activityCommandSummary = cmdSummary;
224
+ }
225
+ let thinkingDisplay = null;
226
+ if (result.thinking) {
227
+ thinkingDisplay = formatThinkingForDisplay(result.thinking, 1600);
228
+ }
229
+ let statusLines = null;
230
+ if (result.incomplete) {
231
+ statusLines = [];
232
+ if (result.stopReason === 'max_tokens')
233
+ statusLines.push('Output limit reached. Response may be truncated.');
234
+ if (result.stopReason === 'timeout') {
235
+ statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(elapsedMs)))} before the agent reported completion.`);
236
+ }
237
+ if (!result.ok) {
238
+ const detail = result.error?.trim();
239
+ if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
240
+ statusLines.push(detail);
241
+ else if (result.stopReason !== 'timeout')
242
+ statusLines.push('Agent exited before reporting completion.');
243
+ }
244
+ }
245
+ return {
246
+ footerStatus,
247
+ activityNarrative,
248
+ activityCommandSummary,
249
+ thinkingDisplay,
250
+ thinkLabel: thinkLabel(agent),
251
+ statusLines,
252
+ bodyMessage: result.message,
253
+ elapsedMs,
254
+ };
255
+ }
256
+ /**
257
+ * Build the sub-agent block for the streaming preview. Sub-agents are
258
+ * deliberately isolated from parent activity (their tool list lives on each
259
+ * StreamSubAgent record), so we render a separate compact section showing each
260
+ * sub-agent's purpose + latest tool. Completed sub-agents are hidden — the
261
+ * parent's activity already reflects the Task `done` line.
262
+ */
263
+ function renderSubAgentsForPreview(meta) {
264
+ const subs = meta?.subAgents;
265
+ if (!subs?.length)
266
+ return '';
267
+ const lines = [];
268
+ for (const sub of subs) {
269
+ if (sub.status !== 'running')
270
+ continue;
271
+ const label = (sub.description || sub.kind || 'sub-agent').trim().slice(0, 80);
272
+ const lastTool = sub.tools.length ? sub.tools[sub.tools.length - 1].summary : 'starting…';
273
+ const modelTag = sub.model ? ` · ${sub.model}` : '';
274
+ lines.push(`↳ ${label}${modelTag}`);
275
+ lines.push(` · ${lastTool}`);
276
+ }
277
+ return lines.join('\n');
278
+ }
279
+ export function extractStreamPreviewData(input) {
280
+ const maxBody = 2400;
281
+ const display = input.bodyText.trim();
282
+ const rawThinking = input.thinking.trim();
283
+ const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
284
+ const planDisplay = renderPlanForPreview(input.plan ?? null);
285
+ const activityDisplay = summarizeActivityForPreview(input.activity);
286
+ const subAgentsDisplay = renderSubAgentsForPreview(input.meta);
287
+ const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
288
+ const label = thinkLabel(input.agent);
289
+ const thinkSnippet = rawThinking ? formatThinkingForDisplay(input.thinking, 600) : '';
290
+ const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
291
+ // Elapsed time is the only monotonic progress signal available during the
292
+ // thinking phase (see thinkingProgressText). Hidden in the first second so a
293
+ // freshly-opened card doesn't flash "0s".
294
+ const elapsedMs = Math.max(0, input.elapsedMs);
295
+ const thinkingProgressText = elapsedMs >= 1000 ? fmtCompactUptime(elapsedMs) : null;
296
+ return {
297
+ display,
298
+ rawThinking,
299
+ thinkDisplay,
300
+ planDisplay,
301
+ activityDisplay,
302
+ subAgentsDisplay,
303
+ maxActivity,
304
+ label,
305
+ thinkSnippet,
306
+ preview,
307
+ thinkingProgressText,
308
+ };
309
+ }
@@ -0,0 +1,361 @@
1
+ /**
2
+ * session-hub.ts — Unified session management service.
3
+ *
4
+ * THE canonical interface for all session operations across pikiloop.
5
+ * Upper-layer code (bot, dashboard, CLI) should import session functions
6
+ * from here, not from code-agent.ts directly.
7
+ *
8
+ * Responsibilities:
9
+ * - Cross-agent / workspace-scoped session queries
10
+ * - Session metadata management (status, notes, links, classification)
11
+ * - Migration, export/import orchestration
12
+ * - Workspace registry (delegates to user-config)
13
+ */
14
+ import path from 'node:path';
15
+ import { getSessions as _getSessions, getSessionTail as _getSessionTail, getSessionMessages as _getSessionMessages, classifySession as _classifySession, deriveUserStatus as _deriveStatusFromOutcome, exportSession as _exportSession, importSession as _importSession, findPikiloopSession, updateSessionMeta, deleteAgentSession as _deleteAgentSession, collapseSkillPrompt, } from '../agent/index.js';
16
+ import { allDriverIds, hasDriver } from '../agent/driver.js';
17
+ import { loadWorkspaces, addWorkspace, removeWorkspace, renameWorkspace, reorderWorkspaces, updateWorkspace, findWorkspace, } from '../core/config/user-config.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Resolve user status
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Compute the effective user status for a session.
23
+ * Priority: explicit userStatus > derived from classification > inbox.
24
+ */
25
+ export function resolveUserStatus(session) {
26
+ if (session.userStatus)
27
+ return session.userStatus;
28
+ if (session.classification)
29
+ return _deriveStatusFromOutcome(session.classification.outcome);
30
+ return 'inbox';
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Unified session query
34
+ // ---------------------------------------------------------------------------
35
+ function normalizeAgents(agent) {
36
+ if (!agent)
37
+ return allDriverIds().filter(a => hasDriver(a));
38
+ const list = Array.isArray(agent) ? agent : [agent];
39
+ return list.filter(a => hasDriver(a));
40
+ }
41
+ /**
42
+ * Query sessions — the single entry point for all session listing.
43
+ *
44
+ * Handles single-agent, multi-agent, and all-agent queries with optional
45
+ * status filtering and limits. Returns workspace-enriched results.
46
+ */
47
+ export async function querySessions(opts) {
48
+ const resolvedWorkdir = path.resolve(opts.workdir);
49
+ const ws = findWorkspace(resolvedWorkdir);
50
+ const workspaceName = ws?.name || path.basename(resolvedWorkdir);
51
+ const agents = normalizeAgents(opts.agent);
52
+ const results = await Promise.all(agents.map(agent => _getSessions({ agent, workdir: resolvedWorkdir }).catch(() => ({
53
+ ok: false, sessions: [], error: `Failed to fetch ${agent} sessions`,
54
+ }))));
55
+ let allSessions = [];
56
+ const errors = [];
57
+ let anyOk = false;
58
+ for (const result of results) {
59
+ if (result.ok)
60
+ anyOk = true;
61
+ if (result.error)
62
+ errors.push(result.error);
63
+ for (const session of result.sessions) {
64
+ allSessions.push({ ...session, workspaceName });
65
+ }
66
+ }
67
+ // Sort by most recent activity
68
+ allSessions.sort((a, b) => {
69
+ const aTime = a.runUpdatedAt || a.createdAt || '';
70
+ const bTime = b.runUpdatedAt || b.createdAt || '';
71
+ return Date.parse(bTime) - Date.parse(aTime);
72
+ });
73
+ // Filter by userStatus
74
+ if (opts.userStatus?.length) {
75
+ const allowed = new Set(opts.userStatus);
76
+ allSessions = allSessions.filter(s => allowed.has(resolveUserStatus(s)));
77
+ }
78
+ // Apply limit
79
+ if (opts.limit && opts.limit > 0) {
80
+ allSessions = allSessions.slice(0, opts.limit);
81
+ }
82
+ // Count statuses
83
+ const statusCounts = { inbox: 0, active: 0, review: 0, done: 0, parked: 0, unknown: 0 };
84
+ for (const s of allSessions) {
85
+ const status = resolveUserStatus(s);
86
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
87
+ }
88
+ return {
89
+ ok: anyOk || agents.length === 0,
90
+ workdir: resolvedWorkdir,
91
+ workspaceName,
92
+ sessions: allSessions,
93
+ statusCounts: statusCounts,
94
+ total: allSessions.length,
95
+ errors,
96
+ };
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Session detail queries
100
+ // ---------------------------------------------------------------------------
101
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
102
+ const MIME_BY_EXT = {
103
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
104
+ '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml',
105
+ };
106
+ /** Build image MessageBlocks from a session record's `lastUserAttachments`
107
+ * (relative paths under `workspacePath`). Used by fallback paths so the
108
+ * dashboard can still render the user's image bubble while the agent CLI
109
+ * has not yet flushed the turn to its own session file. Non-image
110
+ * attachments are skipped — the fallback is text-first and doesn't try to
111
+ * reconstruct generic file references. */
112
+ function imageBlocksFromManagedRecord(record) {
113
+ const attachments = record.lastUserAttachments;
114
+ if (!attachments?.length)
115
+ return [];
116
+ const blocks = [];
117
+ for (const rel of attachments) {
118
+ const ext = path.extname(rel).toLowerCase();
119
+ if (!IMAGE_EXTENSIONS.has(ext))
120
+ continue;
121
+ const abs = path.isAbsolute(rel) ? rel : path.join(record.workspacePath, rel);
122
+ blocks.push({
123
+ type: 'image',
124
+ // `file://` sentinel — `rewriteImageBlocksForTransport` (dashboard
125
+ // response layer) converts it to a proper /attachment URL.
126
+ content: `file://${abs}`,
127
+ imagePath: abs,
128
+ imageMime: MIME_BY_EXT[ext] || 'application/octet-stream',
129
+ });
130
+ }
131
+ return blocks;
132
+ }
133
+ function tailFallbackFromManagedRecord(opts) {
134
+ const fb = managedFallbackContent(opts);
135
+ if (!fb)
136
+ return null;
137
+ const limit = Math.max(1, opts.limit ?? fb.messages.length);
138
+ return { ok: true, messages: fb.messages.slice(-limit), error: null };
139
+ }
140
+ function managedFallbackContent(opts) {
141
+ const record = findPikiloopSession(opts.workdir, opts.agent, opts.sessionId);
142
+ if (!record)
143
+ return null;
144
+ const messages = [];
145
+ const richMessages = [];
146
+ if (record.lastQuestion) {
147
+ const text = record.lastQuestion;
148
+ messages.push({ role: 'user', text });
149
+ const blocks = text ? [{ type: 'text', content: text }] : [];
150
+ blocks.push(...imageBlocksFromManagedRecord(record));
151
+ if (blocks.length)
152
+ richMessages.push({ role: 'user', text, blocks, usage: null });
153
+ }
154
+ const failureText = record.lastAnswer
155
+ || (record.runState === 'incomplete' ? record.runDetail : null);
156
+ if (failureText) {
157
+ messages.push({ role: 'assistant', text: failureText });
158
+ richMessages.push({
159
+ role: 'assistant',
160
+ text: failureText,
161
+ blocks: [{ type: 'text', content: failureText }],
162
+ usage: null,
163
+ });
164
+ }
165
+ if (!messages.length)
166
+ return null;
167
+ return { messages, richMessages };
168
+ }
169
+ /** Get recent messages from a session (tail). */
170
+ export async function querySessionTail(opts) {
171
+ const result = await _getSessionTail(opts);
172
+ if (!result.ok || !result.messages.length) {
173
+ const fallback = tailFallbackFromManagedRecord(opts);
174
+ if (fallback)
175
+ return fallback;
176
+ }
177
+ return result;
178
+ }
179
+ /**
180
+ * Replace canonical skill-execution expansions in a user message with the
181
+ * `/skillname` shorthand the user originally typed. The expanded text is what
182
+ * the agent CLI consumed and persisted; we collapse on read so the dashboard
183
+ * chat shows the slash command instead of the long instruction we synthesized
184
+ * for dispatch. Non-user messages and non-skill prompts pass through unchanged.
185
+ */
186
+ function collapseSkillPromptsInResult(result) {
187
+ if (!result.ok)
188
+ return result;
189
+ const messages = result.messages.map(m => {
190
+ if (m.role !== 'user')
191
+ return m;
192
+ const collapsed = collapseSkillPrompt(m.text);
193
+ return collapsed ? { ...m, text: collapsed } : m;
194
+ });
195
+ const richMessages = result.richMessages?.map(m => {
196
+ if (m.role !== 'user')
197
+ return m;
198
+ const collapsed = collapseSkillPrompt(m.text);
199
+ if (!collapsed)
200
+ return m;
201
+ // The user's text content lives in one or more `text` blocks; collapse any
202
+ // whose individual content also matches the expansion. Non-text blocks
203
+ // (images, attachments) pass through untouched.
204
+ const blocks = m.blocks.map(b => {
205
+ if (b.type !== 'text')
206
+ return b;
207
+ const blockCollapsed = collapseSkillPrompt(b.content);
208
+ return blockCollapsed ? { ...b, content: blockCollapsed } : b;
209
+ });
210
+ return { ...m, text: collapsed, blocks };
211
+ });
212
+ return { ...result, messages, richMessages };
213
+ }
214
+ /** Get full session messages (with optional turn filtering). */
215
+ export async function querySessionMessages(opts) {
216
+ const result = await _getSessionMessages(opts);
217
+ if (!result.ok || !result.messages.length) {
218
+ const fb = managedFallbackContent({
219
+ agent: opts.agent,
220
+ sessionId: opts.sessionId,
221
+ workdir: opts.workdir,
222
+ });
223
+ if (fb) {
224
+ const totalTurns = fb.messages.filter(m => m.role === 'user').length;
225
+ return collapseSkillPromptsInResult({
226
+ ok: true,
227
+ messages: fb.messages.map(m => ({ role: m.role, text: m.text })),
228
+ // Always emit richMessages so the dashboard can render image blocks
229
+ // for the first user turn while the agent CLI is still spinning up.
230
+ richMessages: fb.richMessages,
231
+ totalTurns,
232
+ error: null,
233
+ });
234
+ }
235
+ }
236
+ return collapseSkillPromptsInResult(result);
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Workspace overviews
240
+ // ---------------------------------------------------------------------------
241
+ /** Overview of all registered workspaces — designed for dashboard sidebar. */
242
+ export async function getWorkspaceOverviews() {
243
+ const workspaces = loadWorkspaces();
244
+ const agents = allDriverIds().filter(a => hasDriver(a));
245
+ return Promise.all(workspaces.map(async (ws) => {
246
+ // Fan the agents out in parallel — each _getSessions is independent I/O, and
247
+ // running them serially made one slow agent stall the whole workspace card.
248
+ const summaries = await Promise.all(agents.map(async (agent) => {
249
+ try {
250
+ const result = await _getSessions({ agent, workdir: ws.path });
251
+ let active = 0;
252
+ let review = 0;
253
+ let lastTs = null;
254
+ for (const session of result.sessions) {
255
+ const status = resolveUserStatus(session);
256
+ if (status === 'active' || session.running)
257
+ active++;
258
+ else if (status === 'review')
259
+ review++;
260
+ const ts = session.runUpdatedAt || session.createdAt || '';
261
+ if (ts && (!lastTs || ts > lastTs))
262
+ lastTs = ts;
263
+ }
264
+ return { agent, active, review, total: result.sessions.length, lastTs };
265
+ }
266
+ catch {
267
+ return { agent, active: 0, review: 0, total: 0, lastTs: null };
268
+ }
269
+ }));
270
+ const agentSummary = [];
271
+ let attentionCount = 0;
272
+ let lastActivityAt = null;
273
+ for (const s of summaries) {
274
+ agentSummary.push({ agent: s.agent, active: s.active, review: s.review, total: s.total });
275
+ attentionCount += s.active + s.review;
276
+ if (s.lastTs && (!lastActivityAt || s.lastTs > lastActivityAt))
277
+ lastActivityAt = s.lastTs;
278
+ }
279
+ return { workspace: ws, attentionCount, agentSummary, lastActivityAt };
280
+ }));
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Session metadata
284
+ // ---------------------------------------------------------------------------
285
+ /** Update session metadata (status, note, classification, migration links). */
286
+ export function updateSession(workdir, agent, sessionId, patch) {
287
+ return updateSessionMeta(workdir, agent, sessionId, patch);
288
+ }
289
+ /**
290
+ * Delete a session. Re-exports the agent-layer primitive so dashboard routes
291
+ * stay in the bot/ layer for layering consistency. See
292
+ * {@link DeleteAgentSessionOpts}.
293
+ */
294
+ export function deleteSession(opts) {
295
+ return _deleteAgentSession(opts);
296
+ }
297
+ /** Link two sessions together (bidirectional). */
298
+ export function linkSessions(workdir, a, b) {
299
+ const updatedA = updateSessionMeta(workdir, a.agent, a.sessionId, {
300
+ addLink: { agent: b.agent, sessionId: b.sessionId },
301
+ });
302
+ const updatedB = updateSessionMeta(workdir, b.agent, b.sessionId, {
303
+ addLink: { agent: a.agent, sessionId: a.sessionId },
304
+ });
305
+ return updatedA || updatedB;
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Classification
309
+ // ---------------------------------------------------------------------------
310
+ /** Auto-classify a session based on stream result. */
311
+ export function classifySession(result) {
312
+ return _classifySession(result);
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Export / Import / Migration
316
+ // ---------------------------------------------------------------------------
317
+ export function exportSession(opts) {
318
+ return _exportSession(opts);
319
+ }
320
+ export function importSession(opts) {
321
+ return _importSession(opts);
322
+ }
323
+ /** Build migration context from source session for injection into target agent. */
324
+ export async function buildMigrationContext(opts) {
325
+ try {
326
+ const messagesResult = await _getSessionMessages({
327
+ agent: opts.source.agent,
328
+ sessionId: opts.source.sessionId,
329
+ workdir: opts.source.workdir,
330
+ lastNTurns: opts.lastNTurns,
331
+ });
332
+ if (!messagesResult.ok) {
333
+ return { ok: false, contextInjected: '', messageCount: 0, error: messagesResult.error };
334
+ }
335
+ const messages = messagesResult.messages;
336
+ if (!messages.length) {
337
+ return { ok: false, contextInjected: '', messageCount: 0, error: 'No messages to migrate' };
338
+ }
339
+ const contextLines = [
340
+ `[Migrated from ${opts.source.agent} session, ${messages.length} messages]`,
341
+ '',
342
+ ];
343
+ for (const msg of messages) {
344
+ contextLines.push(`[${msg.role === 'user' ? 'User' : 'Assistant'}]:`);
345
+ contextLines.push(msg.text);
346
+ contextLines.push('');
347
+ }
348
+ const contextInjected = contextLines.join('\n');
349
+ updateSessionMeta(opts.source.workdir, opts.source.agent, opts.source.sessionId, {
350
+ migratedTo: { agent: opts.target.agent, sessionId: '' },
351
+ });
352
+ return { ok: true, contextInjected, messageCount: messages.length, error: null };
353
+ }
354
+ catch (e) {
355
+ return { ok: false, contextInjected: '', messageCount: 0, error: e.message };
356
+ }
357
+ }
358
+ // ---------------------------------------------------------------------------
359
+ // Workspace registry (delegates to user-config)
360
+ // ---------------------------------------------------------------------------
361
+ export { loadWorkspaces, addWorkspace, removeWorkspace, renameWorkspace, reorderWorkspaces, updateWorkspace, findWorkspace };