pikiclaw 0.2.62 → 0.2.63

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.
@@ -3,8 +3,9 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { getAgentLabel, getAgentPackage } from './agent-npm.js';
6
- const AGENT_UPDATE_LOCK_STALE_MS = 60 * 60_000;
7
- const AGENT_UPDATE_COMMAND_TIMEOUT_MS = 15 * 60_000;
6
+ import { AGENT_UPDATE_TIMEOUTS } from './constants.js';
7
+ const AGENT_UPDATE_LOCK_STALE_MS = AGENT_UPDATE_TIMEOUTS.lockStale;
8
+ const AGENT_UPDATE_COMMAND_TIMEOUT_MS = AGENT_UPDATE_TIMEOUTS.commandTimeout;
8
9
  function updaterLockPath() {
9
10
  return path.join(os.homedir(), '.pikiclaw', 'agent-auto-update.lock');
10
11
  }
@@ -95,11 +96,11 @@ async function runCommand(cmd, args, opts = {}) {
95
96
  });
96
97
  }
97
98
  async function getNpmGlobalPrefix() {
98
- const result = await runCommand('npm', ['prefix', '-g'], { timeoutMs: 10_000 });
99
+ const result = await runCommand('npm', ['prefix', '-g'], { timeoutMs: AGENT_UPDATE_TIMEOUTS.npmPrefix });
99
100
  return result.ok ? result.stdout.trim().split('\n')[0] || null : null;
100
101
  }
101
102
  async function getLatestPackageVersion(pkg) {
102
- const result = await runCommand('npm', ['view', pkg, 'version', '--json'], { timeoutMs: 20_000 });
103
+ const result = await runCommand('npm', ['view', pkg, 'version', '--json'], { timeoutMs: AGENT_UPDATE_TIMEOUTS.npmView });
103
104
  if (!result.ok)
104
105
  return null;
105
106
  const raw = result.stdout.trim();
@@ -5,64 +5,21 @@
5
5
  * Also provides a LivePreviewRenderer for streaming output.
6
6
  */
7
7
  import { encodeCommandAction } from './bot-command-ui.js';
8
- import { fmtUptime, fmtTokens, fmtBytes, formatThinkingForDisplay, thinkLabel } from './bot.js';
8
+ import { fmtUptime, fmtTokens, fmtBytes } from './bot.js';
9
9
  import { summarizePromptForStatus } from './bot-commands.js';
10
- import { formatProviderUsageLines } from './bot-telegram-render.js';
11
- import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
10
+ import { footerStatusSymbol, formatFooterSummary, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, } from './bot-render-shared.js';
12
11
  import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from './human-loop.js';
13
12
  import path from 'node:path';
14
13
  import { listSubdirs } from './bot.js';
15
14
  // ---------------------------------------------------------------------------
16
15
  // Helpers
17
16
  // ---------------------------------------------------------------------------
18
- function fmtCompactUptime(ms) {
19
- return fmtUptime(ms).replace(/\s+/g, '');
20
- }
21
- function footerStatusSymbol(status) {
22
- switch (status) {
23
- case 'running': return '●';
24
- case 'done': return '✓';
25
- case 'failed': return '✗';
26
- }
27
- }
28
- function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
29
- const parts = [agent];
30
- const ctx = contextPercent ?? meta?.contextPercent ?? null;
31
- if (ctx != null)
32
- parts.push(`${ctx}%`);
33
- parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
34
- return parts.join(' · ');
35
- }
36
17
  function formatPreviewFooter(agent, elapsedMs, meta) {
37
18
  return `${footerStatusSymbol('running')} ${formatFooterSummary(agent, elapsedMs, meta)}`;
38
19
  }
39
20
  function formatFinalFooter(status, agent, elapsedMs, contextPercent) {
40
21
  return `${footerStatusSymbol(status)} ${formatFooterSummary(agent, elapsedMs, null, contextPercent ?? null)}`;
41
22
  }
42
- function trimActivityForPreview(text, maxChars = 900) {
43
- if (text.length <= maxChars)
44
- return text;
45
- const lines = text.split('\n').filter(l => l.trim());
46
- if (lines.length <= 1)
47
- return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
48
- const tailCount = Math.min(2, Math.max(1, lines.length - 1));
49
- const tail = lines.slice(-tailCount);
50
- const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
51
- const reserved = tail.join('\n').length + 5;
52
- const budget = Math.max(0, maxChars - reserved);
53
- const head = [];
54
- let used = 0;
55
- for (const line of headCandidates) {
56
- const extra = line.length + (head.length ? 1 : 0);
57
- if (used + extra > budget)
58
- break;
59
- head.push(line);
60
- used += extra;
61
- }
62
- if (!head.length)
63
- return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
64
- return [...head, '...', ...tail].join('\n');
65
- }
66
23
  function truncateLabel(label, maxChars = 24) {
67
24
  return label.length > maxChars ? `${label.slice(0, Math.max(1, maxChars - 1))}…` : label;
68
25
  }
@@ -212,31 +169,22 @@ export function buildInitialPreviewMarkdown(agent, model, effort, waiting = fals
212
169
  return parts.join(' · ');
213
170
  }
214
171
  function buildPreviewMarkdown(input, options) {
215
- const maxBody = 2400;
216
- const display = input.bodyText.trim();
217
- const rawThinking = input.thinking.trim();
218
- const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
219
- const planDisplay = renderPlanForPreview(input.plan ?? null);
220
- const activityDisplay = summarizeActivityForPreview(input.activity);
221
- const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
172
+ const data = extractStreamPreviewData(input);
222
173
  const parts = [];
223
- const label = thinkLabel(input.agent);
224
- if (planDisplay) {
225
- parts.push(`**Plan**\n${planDisplay}`);
174
+ if (data.planDisplay) {
175
+ parts.push(`**Plan**\n${data.planDisplay}`);
226
176
  }
227
- if (activityDisplay) {
228
- parts.push(`**Activity**\n${trimActivityForPreview(activityDisplay, maxActivity)}`);
177
+ if (data.activityDisplay) {
178
+ parts.push(`**Activity**\n${trimActivityForPreview(data.activityDisplay, data.maxActivity)}`);
229
179
  }
230
- if (thinkDisplay && !display) {
231
- parts.push(`**${label}**\n${thinkDisplay}`);
180
+ if (data.thinkDisplay && !data.display) {
181
+ parts.push(`**${data.label}**\n${data.thinkDisplay}`);
232
182
  }
233
- else if (display) {
234
- if (rawThinking) {
235
- const thinkSnippet = formatThinkingForDisplay(input.thinking, 600);
236
- parts.push(`**${label}**\n${thinkSnippet}`);
183
+ else if (data.display) {
184
+ if (data.rawThinking) {
185
+ parts.push(`**${data.label}**\n${data.thinkSnippet}`);
237
186
  }
238
- const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
239
- parts.push(preview);
187
+ parts.push(data.preview);
240
188
  }
241
189
  if (options?.includeFooter !== false) {
242
190
  parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null));
@@ -260,46 +208,26 @@ export const feishuStreamingPreviewRenderer = {
260
208
  renderStream: buildStreamingBodyMarkdown,
261
209
  };
262
210
  export function buildFinalReplyRender(agent, result) {
263
- const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
264
- const footerText = `\n\n${formatFinalFooter(footerStatus, agent, result.elapsedS * 1000, result.contextPercent ?? null)}`;
211
+ const data = extractFinalReplyData(agent, result);
212
+ const footerText = `\n\n${formatFinalFooter(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null)}`;
265
213
  let activityText = '';
266
214
  let activityNoteText = '';
267
- if (result.activity) {
268
- const summary = parseActivitySummary(result.activity);
269
- const narrative = summary.narrative.join('\n');
270
- if (narrative) {
271
- let display = narrative;
272
- if (display.length > 1600)
273
- display = '...\n' + display.slice(-1600);
274
- activityText = `**Activity**\n${display}\n\n`;
275
- }
276
- const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
277
- if (commandSummary)
278
- activityNoteText = `*${commandSummary}*\n\n`;
215
+ if (data.activityNarrative) {
216
+ activityText = `**Activity**\n${data.activityNarrative}\n\n`;
217
+ }
218
+ if (data.activityCommandSummary) {
219
+ activityNoteText = `*${data.activityCommandSummary}*\n\n`;
279
220
  }
280
221
  let thinkingText = '';
281
- if (result.thinking) {
282
- thinkingText = `**${thinkLabel(agent)}**\n${formatThinkingForDisplay(result.thinking, 1600)}\n\n`;
222
+ if (data.thinkingDisplay) {
223
+ thinkingText = `**${data.thinkLabel}**\n${data.thinkingDisplay}\n\n`;
283
224
  }
284
225
  let statusText = '';
285
- if (result.incomplete) {
286
- const statusLines = [];
287
- if (result.stopReason === 'max_tokens')
288
- statusLines.push('Output limit reached. Response may be truncated.');
289
- if (result.stopReason === 'timeout') {
290
- statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(result.elapsedS * 1000)))} before the agent reported completion.`);
291
- }
292
- if (!result.ok) {
293
- const detail = result.error?.trim();
294
- if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
295
- statusLines.push(detail);
296
- else if (result.stopReason !== 'timeout')
297
- statusLines.push('Agent exited before reporting completion.');
298
- }
299
- statusText = `**⚠ Incomplete Response**\n${statusLines.join('\n')}\n\n`;
226
+ if (data.statusLines) {
227
+ statusText = `**⚠ Incomplete Response**\n${data.statusLines.join('\n')}\n\n`;
300
228
  }
301
229
  const headerText = `${activityText}${activityNoteText}${statusText}${thinkingText}`;
302
- const bodyText = result.message;
230
+ const bodyText = data.bodyMessage;
303
231
  return {
304
232
  fullText: `${headerText}${bodyText}${footerText}`,
305
233
  headerText,
@@ -474,12 +402,11 @@ export function renderStatus(d) {
474
402
  lines.push(`**Running:** ${fmtUptime(Date.now() - d.running.startedAt)} - ${summarizePromptForStatus(d.running.prompt)}`);
475
403
  }
476
404
  // Provider usage
477
- const usageLines = formatProviderUsageLines(d.usage);
405
+ const usageLines = buildProviderUsageLines(d.usage);
478
406
  if (usageLines.length > 1) {
479
407
  lines.push('');
480
- // Strip HTML tags from usage lines (they're HTML-formatted)
481
408
  for (const line of usageLines) {
482
- lines.push(line.replace(/<\/?[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>'));
409
+ lines.push(line.text);
483
410
  }
484
411
  }
485
412
  lines.push('', '**Bot Usage**', ` Turns: ${d.stats.totalTurns}`);
@@ -24,6 +24,7 @@ import { currentHumanLoopQuestion, humanLoopOptionSelected } from './human-loop.
24
24
  import { FeishuChannel } from './channel-feishu.js';
25
25
  import { splitText, supportsChannelCapability } from './channel-base.js';
26
26
  import { getActiveUserConfig } from './user-config.js';
27
+ import { FEISHU_BOT_CARD_MAX } from './constants.js';
27
28
  const SHUTDOWN_EXIT_CODE = {
28
29
  SIGINT: 130,
29
30
  SIGTERM: 143,
@@ -640,7 +641,7 @@ export class FeishuBot extends Bot {
640
641
  async sendFinalReply(ctx, placeholderId, agent, result) {
641
642
  const rendered = buildFinalReplyRender(agent, result);
642
643
  const messageIds = [];
643
- const MAX_CARD = 25_000;
644
+ const MAX_CARD = FEISHU_BOT_CARD_MAX;
644
645
  if (rendered.fullText.length <= MAX_CARD) {
645
646
  // Fits in one card — edit the placeholder
646
647
  if (placeholderId) {
@@ -1,5 +1,6 @@
1
1
  import { buildDefaultMenuCommands } from './bot-menu.js';
2
- export const BOT_SHUTDOWN_FORCE_EXIT_MS = 3_000;
2
+ import { BOT_SHUTDOWN_FORCE_EXIT_MS as _BOT_SHUTDOWN_FORCE_EXIT_MS } from './constants.js';
3
+ export const BOT_SHUTDOWN_FORCE_EXIT_MS = _BOT_SHUTDOWN_FORCE_EXIT_MS;
3
4
  export function buildBotMenuState(bot) {
4
5
  const agents = bot.fetchAgents().agents;
5
6
  const installedCount = agents.filter(agent => agent.installed).length;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * bot-render-shared.ts — Shared rendering logic used by both Telegram and Feishu 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 { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
8
+ import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
9
+ // ---------------------------------------------------------------------------
10
+ // Footer helpers
11
+ // ---------------------------------------------------------------------------
12
+ export function fmtCompactUptime(ms) {
13
+ return fmtUptime(ms).replace(/\s+/g, '');
14
+ }
15
+ export function footerStatusSymbol(status) {
16
+ switch (status) {
17
+ case 'running': return '●';
18
+ case 'done': return '✓';
19
+ case 'failed': return '✗';
20
+ }
21
+ }
22
+ export function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
23
+ const parts = [agent];
24
+ const ctx = contextPercent ?? meta?.contextPercent ?? null;
25
+ if (ctx != null)
26
+ parts.push(`${ctx}%`);
27
+ parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
28
+ return parts.join(' · ');
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // Activity trimming
32
+ // ---------------------------------------------------------------------------
33
+ export function trimActivityForPreview(text, maxChars = 900) {
34
+ if (text.length <= maxChars)
35
+ return text;
36
+ const lines = text.split('\n').filter(line => line.trim());
37
+ if (lines.length <= 1)
38
+ return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
39
+ const tailCount = Math.min(2, Math.max(1, lines.length - 1));
40
+ const tail = lines.slice(-tailCount);
41
+ const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
42
+ const reserved = tail.join('\n').length + 5;
43
+ const budget = Math.max(0, maxChars - reserved);
44
+ const head = [];
45
+ let used = 0;
46
+ for (const line of headCandidates) {
47
+ const extra = line.length + (head.length ? 1 : 0);
48
+ if (used + extra > budget)
49
+ break;
50
+ head.push(line);
51
+ used += extra;
52
+ }
53
+ if (!head.length)
54
+ return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
55
+ return [...head, '...', ...tail].join('\n');
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Provider usage (plain-text builder — caller wraps as needed)
59
+ // ---------------------------------------------------------------------------
60
+ function rawUsageLine(parts) {
61
+ return parts.filter(part => !!part && String(part).trim()).join(' ');
62
+ }
63
+ export function buildProviderUsageLines(usage) {
64
+ const lines = [
65
+ { text: '', bold: false },
66
+ { text: 'Provider Usage', bold: true },
67
+ ];
68
+ if (!usage.ok) {
69
+ lines.push({ text: ` Unavailable: ${usage.error || 'No recent usage data found.'}` });
70
+ return lines;
71
+ }
72
+ if (usage.capturedAt) {
73
+ const capturedAtMs = Date.parse(usage.capturedAt);
74
+ if (Number.isFinite(capturedAtMs)) {
75
+ lines.push({ text: ` Updated: ${fmtUptime(Math.max(0, Date.now() - capturedAtMs))} ago` });
76
+ }
77
+ }
78
+ if (!usage.windows.length) {
79
+ lines.push({ text: ` ${usage.status ? `status=${usage.status}` : 'No window data'}` });
80
+ return lines;
81
+ }
82
+ for (const window of usage.windows) {
83
+ const details = rawUsageLine([
84
+ window.usedPercent != null ? `${window.usedPercent}% used` : null,
85
+ window.status ? `status=${window.status}` : null,
86
+ window.resetAfterSeconds != null ? `resetAfterSeconds=${window.resetAfterSeconds}` : null,
87
+ ]);
88
+ lines.push({ text: ` ${window.label}: ${details || 'No details'}` });
89
+ }
90
+ return lines;
91
+ }
92
+ export function extractFinalReplyData(agent, result) {
93
+ const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
94
+ const elapsedMs = result.elapsedS * 1000;
95
+ const footerSummary = formatFooterSummary(agent, elapsedMs, null, result.contextPercent ?? null);
96
+ let activityNarrative = null;
97
+ let activityCommandSummary = null;
98
+ if (result.activity) {
99
+ const summary = parseActivitySummary(result.activity);
100
+ const narrative = summary.narrative.join('\n');
101
+ if (narrative) {
102
+ activityNarrative = narrative.length > 1600 ? '...\n' + narrative.slice(-1600) : narrative;
103
+ }
104
+ const cmdSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
105
+ if (cmdSummary)
106
+ activityCommandSummary = cmdSummary;
107
+ }
108
+ let thinkingDisplay = null;
109
+ if (result.thinking) {
110
+ thinkingDisplay = formatThinkingForDisplay(result.thinking, 1600);
111
+ }
112
+ let statusLines = null;
113
+ if (result.incomplete) {
114
+ statusLines = [];
115
+ if (result.stopReason === 'max_tokens')
116
+ statusLines.push('Output limit reached. Response may be truncated.');
117
+ if (result.stopReason === 'timeout') {
118
+ statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(elapsedMs)))} before the agent reported completion.`);
119
+ }
120
+ if (!result.ok) {
121
+ const detail = result.error?.trim();
122
+ if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
123
+ statusLines.push(detail);
124
+ else if (result.stopReason !== 'timeout')
125
+ statusLines.push('Agent exited before reporting completion.');
126
+ }
127
+ }
128
+ return {
129
+ footerStatus,
130
+ footerSummary,
131
+ activityNarrative,
132
+ activityCommandSummary,
133
+ thinkingDisplay,
134
+ thinkLabel: thinkLabel(agent),
135
+ statusLines,
136
+ bodyMessage: result.message,
137
+ elapsedMs,
138
+ };
139
+ }
140
+ export function extractStreamPreviewData(input) {
141
+ const maxBody = 2400;
142
+ const display = input.bodyText.trim();
143
+ const rawThinking = input.thinking.trim();
144
+ const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
145
+ const planDisplay = renderPlanForPreview(input.plan ?? null);
146
+ const activityDisplay = summarizeActivityForPreview(input.activity);
147
+ const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
148
+ const label = thinkLabel(input.agent);
149
+ const thinkSnippet = rawThinking ? formatThinkingForDisplay(input.thinking, 600) : '';
150
+ const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
151
+ return {
152
+ display,
153
+ rawThinking,
154
+ thinkDisplay,
155
+ planDisplay,
156
+ activityDisplay,
157
+ maxActivity,
158
+ label,
159
+ thinkSnippet,
160
+ preview,
161
+ };
162
+ }
@@ -1,7 +1,8 @@
1
1
  import { hasPreviewMeta, samePreviewMeta, samePreviewPlan } from './bot-streaming.js';
2
- const STREAM_PREVIEW_HEARTBEAT_MS = 5_000;
3
- const STREAM_TYPING_HEARTBEAT_MS = 4_000;
4
- const STREAM_STALLED_NOTICE_MS = 15_000;
2
+ import { STREAM_PREVIEW_TIMEOUTS } from './constants.js';
3
+ const STREAM_PREVIEW_HEARTBEAT_MS = STREAM_PREVIEW_TIMEOUTS.heartbeat;
4
+ const STREAM_TYPING_HEARTBEAT_MS = STREAM_PREVIEW_TIMEOUTS.typing;
5
+ const STREAM_STALLED_NOTICE_MS = STREAM_PREVIEW_TIMEOUTS.stalledNotice;
5
6
  // ---------------------------------------------------------------------------
6
7
  // LivePreview — generic streaming preview controller
7
8
  // ---------------------------------------------------------------------------
@@ -1,7 +1,6 @@
1
1
  import { encodeCommandAction } from './bot-command-ui.js';
2
- import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
3
- import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
4
2
  import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from './human-loop.js';
3
+ import { footerStatusSymbol, formatFooterSummary, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, } from './bot-render-shared.js';
5
4
  export function escapeHtml(t) {
6
5
  return t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7
6
  }
@@ -250,82 +249,14 @@ export function renderSkillsListHtml(d) {
250
249
  }
251
250
  return lines.join('\n');
252
251
  }
253
- function fmtCompactUptime(ms) {
254
- return fmtUptime(ms).replace(/\s+/g, '');
255
- }
256
- function footerStatusSymbol(status) {
257
- switch (status) {
258
- case 'running': return '●';
259
- case 'done': return '✓';
260
- case 'failed': return '✗';
261
- }
262
- }
263
- function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
264
- const parts = [agent];
265
- const ctx = contextPercent ?? meta?.contextPercent ?? null;
266
- if (ctx != null)
267
- parts.push(`${ctx}%`);
268
- parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
269
- return parts.join(' · ');
270
- }
271
252
  export function formatPreviewFooterHtml(agent, elapsedMs, meta) {
272
253
  return escapeHtml(`${footerStatusSymbol('running')} ${formatFooterSummary(agent, elapsedMs, meta)}`);
273
254
  }
274
255
  function formatFinalFooterHtml(status, agent, elapsedMs, contextPercent) {
275
256
  return escapeHtml(`${footerStatusSymbol(status)} ${formatFooterSummary(agent, elapsedMs, null, contextPercent ?? null)}`);
276
257
  }
277
- function rawUsageLine(parts) {
278
- return parts.filter(part => !!part && String(part).trim()).join(' ');
279
- }
280
258
  export function formatProviderUsageLines(usage) {
281
- const lines = ['', '<b>Provider Usage</b>'];
282
- if (!usage.ok) {
283
- lines.push(` Unavailable: ${escapeHtml(usage.error || 'No recent usage data found.')}`);
284
- return lines;
285
- }
286
- if (usage.capturedAt) {
287
- const capturedAtMs = Date.parse(usage.capturedAt);
288
- if (Number.isFinite(capturedAtMs)) {
289
- lines.push(` Updated: ${fmtUptime(Math.max(0, Date.now() - capturedAtMs))} ago`);
290
- }
291
- }
292
- if (!usage.windows.length) {
293
- lines.push(` ${escapeHtml(usage.status ? `status=${usage.status}` : 'No window data')}`);
294
- return lines;
295
- }
296
- for (const window of usage.windows) {
297
- const details = rawUsageLine([
298
- window.usedPercent != null ? `${window.usedPercent}% used` : null,
299
- window.status ? `status=${window.status}` : null,
300
- window.resetAfterSeconds != null ? `resetAfterSeconds=${window.resetAfterSeconds}` : null,
301
- ]);
302
- lines.push(` ${escapeHtml(window.label)}: ${escapeHtml(details || 'No details')}`);
303
- }
304
- return lines;
305
- }
306
- function trimActivityForPreview(text, maxChars = 900) {
307
- if (text.length <= maxChars)
308
- return text;
309
- const lines = text.split('\n').filter(line => line.trim());
310
- if (lines.length <= 1)
311
- return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
312
- const tailCount = Math.min(2, Math.max(1, lines.length - 1));
313
- const tail = lines.slice(-tailCount);
314
- const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
315
- const reserved = tail.join('\n').length + 5;
316
- const budget = Math.max(0, maxChars - reserved);
317
- const head = [];
318
- let used = 0;
319
- for (const line of headCandidates) {
320
- const extra = line.length + (head.length ? 1 : 0);
321
- if (used + extra > budget)
322
- break;
323
- head.push(line);
324
- used += extra;
325
- }
326
- if (!head.length)
327
- return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
328
- return [...head, '...', ...tail].join('\n');
259
+ return buildProviderUsageLines(usage).map(line => line.bold ? `<b>${escapeHtml(line.text)}</b>` : escapeHtml(line.text));
329
260
  }
330
261
  export function buildInitialPreviewHtml(agent, waiting = false) {
331
262
  return waiting
@@ -333,76 +264,47 @@ export function buildInitialPreviewHtml(agent, waiting = false) {
333
264
  : formatPreviewFooterHtml(agent, 0);
334
265
  }
335
266
  export function buildStreamPreviewHtml(input) {
336
- const maxBody = 2400;
337
- const display = input.bodyText.trim();
338
- const rawThinking = input.thinking.trim();
339
- const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
340
- const planDisplay = renderPlanForPreview(input.plan ?? null);
341
- const activityDisplay = summarizeActivityForPreview(input.activity);
342
- const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
267
+ const data = extractStreamPreviewData(input);
343
268
  const parts = [];
344
- const label = thinkLabel(input.agent);
345
- if (planDisplay) {
346
- parts.push(`<blockquote><b>Plan</b>\n${escapeHtml(planDisplay)}</blockquote>`);
269
+ if (data.planDisplay) {
270
+ parts.push(`<blockquote><b>Plan</b>\n${escapeHtml(data.planDisplay)}</blockquote>`);
347
271
  }
348
- if (activityDisplay) {
349
- parts.push(`<blockquote><b>Activity</b>\n${escapeHtml(trimActivityForPreview(activityDisplay, maxActivity))}</blockquote>`);
272
+ if (data.activityDisplay) {
273
+ parts.push(`<blockquote><b>Activity</b>\n${escapeHtml(trimActivityForPreview(data.activityDisplay, data.maxActivity))}</blockquote>`);
350
274
  }
351
- if (thinkDisplay && !display) {
352
- parts.push(`<blockquote><b>${escapeHtml(label)}</b>\n${escapeHtml(thinkDisplay)}</blockquote>`);
275
+ if (data.thinkDisplay && !data.display) {
276
+ parts.push(`<blockquote><b>${escapeHtml(data.label)}</b>\n${escapeHtml(data.thinkDisplay)}</blockquote>`);
353
277
  }
354
- else if (display) {
355
- if (rawThinking) {
356
- const thinkSnippet = formatThinkingForDisplay(input.thinking, 600);
357
- parts.push(`<blockquote><b>${escapeHtml(label)}</b>\n${escapeHtml(thinkSnippet)}</blockquote>`);
278
+ else if (data.display) {
279
+ if (data.rawThinking) {
280
+ parts.push(`<blockquote><b>${escapeHtml(data.label)}</b>\n${escapeHtml(data.thinkSnippet)}</blockquote>`);
358
281
  }
359
- const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
360
- parts.push(mdToTgHtml(preview));
282
+ parts.push(mdToTgHtml(data.preview));
361
283
  }
362
284
  parts.push(formatPreviewFooterHtml(input.agent, input.elapsedMs, input.meta ?? null));
363
285
  return parts.join('\n\n');
364
286
  }
365
287
  export function buildFinalReplyRender(agent, result) {
366
- const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
367
- const footerHtml = `\n\n${formatFinalFooterHtml(footerStatus, agent, result.elapsedS * 1000, result.contextPercent ?? null)}`;
288
+ const data = extractFinalReplyData(agent, result);
289
+ const footerHtml = `\n\n${formatFinalFooterHtml(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null)}`;
368
290
  let activityHtml = '';
369
291
  let activityNoteHtml = '';
370
- if (result.activity) {
371
- const summary = parseActivitySummary(result.activity);
372
- const narrative = summary.narrative.join('\n');
373
- if (narrative) {
374
- let display = narrative;
375
- if (display.length > 1600)
376
- display = '...\n' + display.slice(-1600);
377
- activityHtml = `<blockquote><b>Activity</b>\n${escapeHtml(display)}</blockquote>\n\n`;
378
- }
379
- const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
380
- if (commandSummary)
381
- activityNoteHtml = `<i>${escapeHtml(commandSummary)}</i>\n\n`;
292
+ if (data.activityNarrative) {
293
+ activityHtml = `<blockquote><b>Activity</b>\n${escapeHtml(data.activityNarrative)}</blockquote>\n\n`;
294
+ }
295
+ if (data.activityCommandSummary) {
296
+ activityNoteHtml = `<i>${escapeHtml(data.activityCommandSummary)}</i>\n\n`;
382
297
  }
383
298
  let thinkingHtml = '';
384
- if (result.thinking) {
385
- thinkingHtml = `<blockquote><b>${thinkLabel(agent)}</b>\n${escapeHtml(formatThinkingForDisplay(result.thinking, 1600))}</blockquote>\n\n`;
299
+ if (data.thinkingDisplay) {
300
+ thinkingHtml = `<blockquote><b>${escapeHtml(data.thinkLabel)}</b>\n${escapeHtml(data.thinkingDisplay)}</blockquote>\n\n`;
386
301
  }
387
302
  let statusHtml = '';
388
- if (result.incomplete) {
389
- const statusLines = [];
390
- if (result.stopReason === 'max_tokens')
391
- statusLines.push('Output limit reached. Response may be truncated.');
392
- if (result.stopReason === 'timeout') {
393
- statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(result.elapsedS * 1000)))} before the agent reported completion.`);
394
- }
395
- if (!result.ok) {
396
- const detail = result.error?.trim();
397
- if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
398
- statusLines.push(detail);
399
- else if (result.stopReason !== 'timeout')
400
- statusLines.push('Agent exited before reporting completion.');
401
- }
402
- statusHtml = `<blockquote expandable><b>Incomplete Response</b>\n${statusLines.map(escapeHtml).join('\n')}</blockquote>\n\n`;
303
+ if (data.statusLines) {
304
+ statusHtml = `<blockquote expandable><b>Incomplete Response</b>\n${data.statusLines.map(escapeHtml).join('\n')}</blockquote>\n\n`;
403
305
  }
404
306
  const headerHtml = `${activityHtml}${activityNoteHtml}${statusHtml}${thinkingHtml}`;
405
- const bodyHtml = mdToTgHtml(result.message);
307
+ const bodyHtml = mdToTgHtml(data.bodyMessage);
406
308
  return {
407
309
  fullHtml: `${headerHtml}${bodyHtml}${footerHtml}`,
408
310
  headerHtml,
package/dist/bot.js CHANGED
@@ -13,9 +13,10 @@ import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
13
13
  import { terminateProcessTree } from './process-control.js';
14
14
  import { VERSION } from './version.js';
15
15
  import { buildHumanLoopResponse, createEmptyHumanLoopAnswer, currentHumanLoopQuestion, isHumanLoopAwaitingText, setHumanLoopOption, setHumanLoopText, skipHumanLoopQuestion, } from './human-loop.js';
16
- export const DEFAULT_RUN_TIMEOUT_S = 7200;
17
- const MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS = 20_000;
18
- const MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S = 30;
16
+ import { BOT_TIMEOUTS } from './constants.js';
17
+ export const DEFAULT_RUN_TIMEOUT_S = BOT_TIMEOUTS.defaultRunTimeoutS;
18
+ const MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS = BOT_TIMEOUTS.macosUserActivityPulseInterval;
19
+ const MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S = BOT_TIMEOUTS.macosUserActivityPulseTimeoutS;
19
20
  // ---------------------------------------------------------------------------
20
21
  // Helpers
21
22
  // ---------------------------------------------------------------------------
@@ -14,11 +14,12 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { Channel, DEFAULT_CHANNEL_CAPABILITIES, sleep, } from './channel-base.js';
16
16
  import { adaptMarkdownForFeishu } from './bot-feishu-render.js';
17
+ import { FEISHU_LIMITS } from './constants.js';
17
18
  export { FeishuChannel };
18
- const FEISHU_CARD_MAX = 28_000; // card markdown budget (card JSON limit ~30KB)
19
- const FILE_MAX_BYTES = 20 * 1024 * 1024; // 20MB max for file send/receive
19
+ const FEISHU_CARD_MAX = FEISHU_LIMITS.cardMax;
20
+ const FILE_MAX_BYTES = FEISHU_LIMITS.fileMaxBytes;
20
21
  const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
21
- const FEISHU_WS_START_RETRY_MAX_DELAY_MS = 60_000;
22
+ const FEISHU_WS_START_RETRY_MAX_DELAY_MS = FEISHU_LIMITS.wsStartRetryMaxDelay;
22
23
  function describeError(err) {
23
24
  if (!(err instanceof Error))
24
25
  return String(err ?? 'unknown error');
@@ -299,7 +300,7 @@ class FeishuChannel extends Channel {
299
300
  }
300
301
  async listen() {
301
302
  this.running = true;
302
- let retryDelayMs = 3_000;
303
+ let retryDelayMs = FEISHU_LIMITS.wsStartRetryInitialDelay;
303
304
  while (this.running) {
304
305
  const sdkDomain = this.domain.includes('larksuite.com')
305
306
  ? lark.Domain.Lark