pikiclaw 0.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot-feishu-render.ts — Feishu-specific rendering.
|
|
3
|
+
*
|
|
4
|
+
* Converts structured data from bot-commands.ts into Feishu Markdown (for interactive cards).
|
|
5
|
+
* Also provides a LivePreviewRenderer for streaming output.
|
|
6
|
+
*/
|
|
7
|
+
import { encodeCommandAction } from './bot-command-ui.js';
|
|
8
|
+
import { fmtUptime, fmtTokens, fmtBytes, formatThinkingForDisplay, thinkLabel } from './bot.js';
|
|
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';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { listSubdirs } from './bot.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function fmtCompactUptime(ms) {
|
|
18
|
+
return fmtUptime(ms).replace(/\s+/g, '');
|
|
19
|
+
}
|
|
20
|
+
function footerStatusSymbol(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'running': return '●';
|
|
23
|
+
case 'done': return '✓';
|
|
24
|
+
case 'failed': return '✗';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
|
|
28
|
+
const parts = [agent];
|
|
29
|
+
const ctx = contextPercent ?? meta?.contextPercent ?? null;
|
|
30
|
+
if (ctx != null)
|
|
31
|
+
parts.push(`${ctx}%`);
|
|
32
|
+
parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
|
|
33
|
+
return parts.join(' · ');
|
|
34
|
+
}
|
|
35
|
+
function formatPreviewFooter(agent, elapsedMs, meta) {
|
|
36
|
+
return `${footerStatusSymbol('running')} ${formatFooterSummary(agent, elapsedMs, meta)}`;
|
|
37
|
+
}
|
|
38
|
+
function formatFinalFooter(status, agent, elapsedMs, contextPercent) {
|
|
39
|
+
return `${footerStatusSymbol(status)} ${formatFooterSummary(agent, elapsedMs, null, contextPercent ?? null)}`;
|
|
40
|
+
}
|
|
41
|
+
function trimActivityForPreview(text, maxChars = 900) {
|
|
42
|
+
if (text.length <= maxChars)
|
|
43
|
+
return text;
|
|
44
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
45
|
+
if (lines.length <= 1)
|
|
46
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
47
|
+
const tailCount = Math.min(2, Math.max(1, lines.length - 1));
|
|
48
|
+
const tail = lines.slice(-tailCount);
|
|
49
|
+
const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
|
|
50
|
+
const reserved = tail.join('\n').length + 5;
|
|
51
|
+
const budget = Math.max(0, maxChars - reserved);
|
|
52
|
+
const head = [];
|
|
53
|
+
let used = 0;
|
|
54
|
+
for (const line of headCandidates) {
|
|
55
|
+
const extra = line.length + (head.length ? 1 : 0);
|
|
56
|
+
if (used + extra > budget)
|
|
57
|
+
break;
|
|
58
|
+
head.push(line);
|
|
59
|
+
used += extra;
|
|
60
|
+
}
|
|
61
|
+
if (!head.length)
|
|
62
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
63
|
+
return [...head, '...', ...tail].join('\n');
|
|
64
|
+
}
|
|
65
|
+
function truncateLabel(label, maxChars = 24) {
|
|
66
|
+
return label.length > maxChars ? `${label.slice(0, Math.max(1, maxChars - 1))}…` : label;
|
|
67
|
+
}
|
|
68
|
+
function cardButton(label, action, primary = false) {
|
|
69
|
+
const button = {
|
|
70
|
+
tag: 'button',
|
|
71
|
+
text: { tag: 'plain_text', content: truncateLabel(label) },
|
|
72
|
+
value: { action },
|
|
73
|
+
};
|
|
74
|
+
if (primary)
|
|
75
|
+
button.type = 'primary';
|
|
76
|
+
return button;
|
|
77
|
+
}
|
|
78
|
+
function cardRows(actions, size = 3) {
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (let i = 0; i < actions.length; i += size) {
|
|
81
|
+
const rowActions = actions.slice(i, i + size);
|
|
82
|
+
if (!rowActions.length)
|
|
83
|
+
continue;
|
|
84
|
+
rows.push({ actions: rowActions });
|
|
85
|
+
}
|
|
86
|
+
return rows;
|
|
87
|
+
}
|
|
88
|
+
function selectionStateSymbol(state) {
|
|
89
|
+
switch (state) {
|
|
90
|
+
case 'current': return '●';
|
|
91
|
+
case 'running': return '🟢';
|
|
92
|
+
case 'unavailable': return '❌';
|
|
93
|
+
default: return '○';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formatCommandItemMarkdown(item, index) {
|
|
97
|
+
const parts = [
|
|
98
|
+
selectionStateSymbol(item.state),
|
|
99
|
+
`**${index + 1}.**`,
|
|
100
|
+
item.label,
|
|
101
|
+
];
|
|
102
|
+
if (item.detail)
|
|
103
|
+
parts.push(item.detail);
|
|
104
|
+
return parts.join(' ');
|
|
105
|
+
}
|
|
106
|
+
function formatCommandButtonLabel(button) {
|
|
107
|
+
const prefix = button.state && button.state !== 'default'
|
|
108
|
+
? `${selectionStateSymbol(button.state)} `
|
|
109
|
+
: '';
|
|
110
|
+
return truncateLabel(`${prefix}${button.label}`.trim());
|
|
111
|
+
}
|
|
112
|
+
function actionButton(button) {
|
|
113
|
+
return cardButton(formatCommandButtonLabel(button), encodeCommandAction(button.action), !!button.primary);
|
|
114
|
+
}
|
|
115
|
+
export function renderCommandNotice(notice) {
|
|
116
|
+
const lines = [`**${notice.title}**`];
|
|
117
|
+
if (notice.value) {
|
|
118
|
+
lines.push(notice.valueMode === 'plain' ? notice.value : `\`${notice.value}\``);
|
|
119
|
+
}
|
|
120
|
+
if (notice.detail)
|
|
121
|
+
lines.push(notice.detail);
|
|
122
|
+
return lines.join('\n\n');
|
|
123
|
+
}
|
|
124
|
+
export function renderCommandSelectionMarkdown(view) {
|
|
125
|
+
const title = view.detail ? `**${view.title}** · \`${view.detail}\`` : `**${view.title}**`;
|
|
126
|
+
const lines = [title];
|
|
127
|
+
if (view.metaLines.length)
|
|
128
|
+
lines.push(...view.metaLines.map(line => `*${line}*`));
|
|
129
|
+
if (view.items.length) {
|
|
130
|
+
lines.push('', ...view.items.map((item, index) => formatCommandItemMarkdown(item, index)));
|
|
131
|
+
}
|
|
132
|
+
else if (view.emptyText) {
|
|
133
|
+
lines.push('', `*${view.emptyText}*`);
|
|
134
|
+
}
|
|
135
|
+
if (view.helperText)
|
|
136
|
+
lines.push('', `*${view.helperText}*`);
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
export function renderCommandSelectionCard(view) {
|
|
140
|
+
return {
|
|
141
|
+
markdown: renderCommandSelectionMarkdown(view),
|
|
142
|
+
rows: view.rows.map(row => ({ actions: row.map(actionButton) })),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function escapeFeishuMarkdownText(text) {
|
|
146
|
+
return text.replace(/([\\`*_{}[\]()#+\-.!|>~])/g, '\\$1');
|
|
147
|
+
}
|
|
148
|
+
function renderFeishuQuote(text) {
|
|
149
|
+
return text
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map(line => `> ${escapeFeishuMarkdownText(line)}`)
|
|
152
|
+
.join('\n');
|
|
153
|
+
}
|
|
154
|
+
export function renderSessionTurnMarkdown(userText, assistantText) {
|
|
155
|
+
const parts = [];
|
|
156
|
+
const user = String(userText || '').trim();
|
|
157
|
+
const assistant = String(assistantText || '').trim();
|
|
158
|
+
if (user || assistant)
|
|
159
|
+
parts.push('**Recent Context**');
|
|
160
|
+
if (user)
|
|
161
|
+
parts.push('**User**', renderFeishuQuote(user));
|
|
162
|
+
if (assistant)
|
|
163
|
+
parts.push('**Assistant**', assistant);
|
|
164
|
+
return parts.join('\n\n');
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// LivePreview renderer — produces Markdown for Feishu card elements
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
export function buildInitialPreviewMarkdown(agent) {
|
|
170
|
+
return formatPreviewFooter(agent, 0);
|
|
171
|
+
}
|
|
172
|
+
export function buildStreamPreviewMarkdown(input) {
|
|
173
|
+
const maxBody = 2400;
|
|
174
|
+
const display = input.bodyText.trim();
|
|
175
|
+
const rawThinking = input.thinking.trim();
|
|
176
|
+
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
177
|
+
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
178
|
+
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
179
|
+
const maxActivity = !display && !thinkDisplay && !planDisplay ? 1800 : 900;
|
|
180
|
+
const parts = [];
|
|
181
|
+
const label = thinkLabel(input.agent);
|
|
182
|
+
if (planDisplay) {
|
|
183
|
+
parts.push(`**Plan**\n${planDisplay}`);
|
|
184
|
+
}
|
|
185
|
+
if (activityDisplay) {
|
|
186
|
+
parts.push(`**Activity**\n${trimActivityForPreview(activityDisplay, maxActivity)}`);
|
|
187
|
+
}
|
|
188
|
+
if (thinkDisplay && !display) {
|
|
189
|
+
parts.push(`**${label}**\n${thinkDisplay}`);
|
|
190
|
+
}
|
|
191
|
+
else if (display) {
|
|
192
|
+
if (rawThinking)
|
|
193
|
+
parts.push(`*${label} (${rawThinking.length} chars)*`);
|
|
194
|
+
const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
|
|
195
|
+
parts.push(preview);
|
|
196
|
+
}
|
|
197
|
+
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null));
|
|
198
|
+
return parts.join('\n\n');
|
|
199
|
+
}
|
|
200
|
+
export const feishuPreviewRenderer = {
|
|
201
|
+
renderInitial: buildInitialPreviewMarkdown,
|
|
202
|
+
renderStream: buildStreamPreviewMarkdown,
|
|
203
|
+
};
|
|
204
|
+
export function buildFinalReplyRender(agent, result) {
|
|
205
|
+
const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
|
|
206
|
+
const footerText = `\n\n${formatFinalFooter(footerStatus, agent, result.elapsedS * 1000, result.contextPercent ?? null)}`;
|
|
207
|
+
let activityText = '';
|
|
208
|
+
let activityNoteText = '';
|
|
209
|
+
if (result.activity) {
|
|
210
|
+
const summary = parseActivitySummary(result.activity);
|
|
211
|
+
const narrative = summary.narrative.join('\n');
|
|
212
|
+
if (narrative) {
|
|
213
|
+
let display = narrative;
|
|
214
|
+
if (display.length > 800)
|
|
215
|
+
display = '...\n' + display.slice(-800);
|
|
216
|
+
activityText = `**Activity**\n${display}\n\n`;
|
|
217
|
+
}
|
|
218
|
+
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
219
|
+
if (commandSummary)
|
|
220
|
+
activityNoteText = `*${commandSummary}*\n\n`;
|
|
221
|
+
}
|
|
222
|
+
let thinkingText = '';
|
|
223
|
+
if (result.thinking) {
|
|
224
|
+
thinkingText = `**${thinkLabel(agent)}**\n${formatThinkingForDisplay(result.thinking, 800)}\n\n`;
|
|
225
|
+
}
|
|
226
|
+
let statusText = '';
|
|
227
|
+
if (result.incomplete) {
|
|
228
|
+
const statusLines = [];
|
|
229
|
+
if (result.stopReason === 'max_tokens')
|
|
230
|
+
statusLines.push('Output limit reached. Response may be truncated.');
|
|
231
|
+
if (result.stopReason === 'timeout') {
|
|
232
|
+
statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(result.elapsedS * 1000)))} before the agent reported completion.`);
|
|
233
|
+
}
|
|
234
|
+
if (!result.ok) {
|
|
235
|
+
const detail = result.error?.trim();
|
|
236
|
+
if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
|
|
237
|
+
statusLines.push(detail);
|
|
238
|
+
else if (result.stopReason !== 'timeout')
|
|
239
|
+
statusLines.push('Agent exited before reporting completion.');
|
|
240
|
+
}
|
|
241
|
+
statusText = `**⚠ Incomplete Response**\n${statusLines.join('\n')}\n\n`;
|
|
242
|
+
}
|
|
243
|
+
const headerText = `${activityText}${activityNoteText}${statusText}${thinkingText}`;
|
|
244
|
+
const bodyText = result.message;
|
|
245
|
+
return {
|
|
246
|
+
fullText: `${headerText}${bodyText}${footerText}`,
|
|
247
|
+
headerText,
|
|
248
|
+
bodyText,
|
|
249
|
+
footerText,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Command renderers — produce Markdown for Feishu cards
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
export function renderStart(d) {
|
|
256
|
+
const lines = [
|
|
257
|
+
`**${d.title}** v${d.version}`,
|
|
258
|
+
d.subtitle,
|
|
259
|
+
'',
|
|
260
|
+
`**Agent:** ${d.agent}`,
|
|
261
|
+
`**Workdir:** \`${d.workdir}\``,
|
|
262
|
+
'',
|
|
263
|
+
'**Commands**',
|
|
264
|
+
...d.commands.map(c => `/${c.command} — ${c.description}`),
|
|
265
|
+
];
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
export function renderSessionsPage(d) {
|
|
269
|
+
const lines = [
|
|
270
|
+
`**${d.agent} sessions** (${d.total}) p${d.page + 1}/${d.totalPages}`,
|
|
271
|
+
'',
|
|
272
|
+
];
|
|
273
|
+
if (!d.sessions.length) {
|
|
274
|
+
lines.push('*No sessions found.*');
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
for (let i = 0; i < d.sessions.length; i++) {
|
|
278
|
+
const s = d.sessions[i];
|
|
279
|
+
const icon = s.isRunning ? '🟢' : s.isCurrent ? '●' : '○';
|
|
280
|
+
lines.push(`${icon} **${i + 1}.** ${s.title} ${s.time}${s.isCurrent ? ' ← current' : ''}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('*Use the controls below to switch, or reply with session number / "new".*');
|
|
284
|
+
}
|
|
285
|
+
if (d.totalPages > 1) {
|
|
286
|
+
lines.push(`\nPage ${d.page + 1}/${d.totalPages}. Use the page controls below or reply "p2", "p3" etc. to navigate.`);
|
|
287
|
+
}
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
|
290
|
+
export function renderAgentsList(d) {
|
|
291
|
+
const lines = ['**Available Agents**', ''];
|
|
292
|
+
for (const a of d.agents) {
|
|
293
|
+
const status = !a.installed ? '❌' : a.isCurrent ? '●' : '○';
|
|
294
|
+
lines.push(`${status} **${a.agent}**${a.isCurrent ? ' (current)' : ''}`);
|
|
295
|
+
if (a.installed) {
|
|
296
|
+
if (a.version)
|
|
297
|
+
lines.push(` Version: \`${a.version}\``);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
lines.push(' Not installed');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push('*Use the controls below to switch, or reply with agent name (e.g. "claude", "codex").*');
|
|
305
|
+
return lines.join('\n');
|
|
306
|
+
}
|
|
307
|
+
export function renderModelsList(d) {
|
|
308
|
+
const lines = [`**Models for ${d.agent}**`];
|
|
309
|
+
if (d.sources.length)
|
|
310
|
+
lines.push(`*Source: ${d.sources.join(', ')}*`);
|
|
311
|
+
if (d.note)
|
|
312
|
+
lines.push(`*${d.note}*`);
|
|
313
|
+
lines.push('');
|
|
314
|
+
if (!d.models.length) {
|
|
315
|
+
lines.push('*No discoverable models found.*');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
for (let i = 0; i < d.models.length; i++) {
|
|
319
|
+
const m = d.models[i];
|
|
320
|
+
const status = m.isCurrent ? '●' : '○';
|
|
321
|
+
const display = m.alias ? `${m.alias} (${m.id})` : m.id;
|
|
322
|
+
lines.push(`${status} **${i + 1}.** \`${display}\`${m.isCurrent ? ' ← current' : ''}`);
|
|
323
|
+
}
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push('*Use the controls below to switch, or reply with model number / ID.*');
|
|
326
|
+
}
|
|
327
|
+
if (d.effort) {
|
|
328
|
+
lines.push('');
|
|
329
|
+
lines.push(`**Thinking Effort:** \`${d.effort.current}\``);
|
|
330
|
+
lines.push(d.effort.levels.map(l => l.isCurrent ? `**[${l.label}]**` : l.label).join(' | '));
|
|
331
|
+
}
|
|
332
|
+
return lines.join('\n');
|
|
333
|
+
}
|
|
334
|
+
export function renderSkillsList(d) {
|
|
335
|
+
const lines = [`**Project Skills** (${d.skills.length})`, '', `**Agent:** ${d.agent}`, `**Workdir:** \`${d.workdir}\``];
|
|
336
|
+
if (!d.skills.length) {
|
|
337
|
+
lines.push('', '*No project skills found in `.pikiclaw/skills/` or `.claude/commands/`.*');
|
|
338
|
+
return lines.join('\n');
|
|
339
|
+
}
|
|
340
|
+
lines.push('');
|
|
341
|
+
for (const skill of d.skills) {
|
|
342
|
+
lines.push(`**/${skill.command}** — ${skill.label}`);
|
|
343
|
+
if (skill.description)
|
|
344
|
+
lines.push(skill.description);
|
|
345
|
+
}
|
|
346
|
+
lines.push('', '*Tap a button below or send the command directly.*');
|
|
347
|
+
return lines.join('\n');
|
|
348
|
+
}
|
|
349
|
+
export function renderSessionsPageCard(d) {
|
|
350
|
+
const sessionButtons = d.sessions.map(s => {
|
|
351
|
+
const prefix = s.isCurrent ? '● ' : s.isRunning ? '🟢 ' : '';
|
|
352
|
+
return cardButton(`${prefix}${s.title}`, `sess:${s.key}`, s.isCurrent);
|
|
353
|
+
});
|
|
354
|
+
const navButtons = [];
|
|
355
|
+
if (d.page > 0)
|
|
356
|
+
navButtons.push(cardButton(`◀ p${d.page}`, `sp:${d.page - 1}`));
|
|
357
|
+
navButtons.push(cardButton('+ New', 'sess:new'));
|
|
358
|
+
if (d.page < d.totalPages - 1)
|
|
359
|
+
navButtons.push(cardButton(`p${d.page + 2} ▶`, `sp:${d.page + 1}`));
|
|
360
|
+
return {
|
|
361
|
+
markdown: renderSessionsPage(d),
|
|
362
|
+
rows: [
|
|
363
|
+
...cardRows(sessionButtons),
|
|
364
|
+
...(navButtons.length ? [{ actions: navButtons }] : []),
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export function renderAgentsListCard(d) {
|
|
369
|
+
const actions = d.agents
|
|
370
|
+
.filter(a => a.installed)
|
|
371
|
+
.map(a => cardButton(a.isCurrent ? `● ${a.agent}` : a.agent, `ag:${a.agent}`, a.isCurrent));
|
|
372
|
+
return {
|
|
373
|
+
markdown: renderAgentsList(d),
|
|
374
|
+
rows: cardRows(actions),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export function renderModelsListCard(d) {
|
|
378
|
+
const modelRows = cardRows(d.models.map(m => cardButton(m.isCurrent ? `● ${m.alias || m.id}` : (m.alias || m.id), `mod:${m.id}`, m.isCurrent)));
|
|
379
|
+
const effortRows = d.effort
|
|
380
|
+
? cardRows(d.effort.levels.map(l => cardButton(l.isCurrent ? `● ${l.label}` : l.label, `eff:${l.id}`, l.isCurrent)))
|
|
381
|
+
: [];
|
|
382
|
+
return {
|
|
383
|
+
markdown: renderModelsList(d),
|
|
384
|
+
rows: [...modelRows, ...effortRows],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
export function renderSkillsCard(d) {
|
|
388
|
+
return {
|
|
389
|
+
markdown: renderSkillsList(d),
|
|
390
|
+
rows: cardRows(d.skills.map(skill => cardButton(skill.label, `skill:${skill.command}`)), 2),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
export function renderStatus(d) {
|
|
394
|
+
const lines = [
|
|
395
|
+
`**pikiclaw** v${d.version}`,
|
|
396
|
+
'',
|
|
397
|
+
`**Uptime:** ${fmtUptime(d.uptime)}`,
|
|
398
|
+
`**Memory:** ${(d.memRss / 1024 / 1024).toFixed(0)}MB RSS / ${(d.memHeap / 1024 / 1024).toFixed(0)}MB heap`,
|
|
399
|
+
`**PID:** ${d.pid}`,
|
|
400
|
+
`**Workdir:** \`${d.workdir}\``,
|
|
401
|
+
'',
|
|
402
|
+
`**Agent:** ${d.agent}`,
|
|
403
|
+
`**Model:** ${d.model}`,
|
|
404
|
+
`**Session:** ${d.sessionId ? `\`${d.sessionId.slice(0, 16)}\`` : '(new)'}`,
|
|
405
|
+
`**Active Tasks:** ${d.activeTasksCount}`,
|
|
406
|
+
];
|
|
407
|
+
if (d.running) {
|
|
408
|
+
lines.push(`**Running:** ${fmtUptime(Date.now() - d.running.startedAt)} - ${summarizePromptForStatus(d.running.prompt)}`);
|
|
409
|
+
}
|
|
410
|
+
// Provider usage
|
|
411
|
+
const usageLines = formatProviderUsageLines(d.usage);
|
|
412
|
+
if (usageLines.length > 1) {
|
|
413
|
+
lines.push('');
|
|
414
|
+
// Strip HTML tags from usage lines (they're HTML-formatted)
|
|
415
|
+
for (const line of usageLines) {
|
|
416
|
+
lines.push(line.replace(/<\/?[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
lines.push('', '**Bot Usage**', ` Turns: ${d.stats.totalTurns}`);
|
|
420
|
+
if (d.stats.totalInputTokens || d.stats.totalOutputTokens) {
|
|
421
|
+
lines.push(` In: ${fmtTokens(d.stats.totalInputTokens)} Out: ${fmtTokens(d.stats.totalOutputTokens)}`);
|
|
422
|
+
if (d.stats.totalCachedTokens)
|
|
423
|
+
lines.push(` Cached: ${fmtTokens(d.stats.totalCachedTokens)}`);
|
|
424
|
+
}
|
|
425
|
+
return lines.join('\n');
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Directory browser (interactive workdir switcher)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
class PathRegistry {
|
|
431
|
+
pathToId = new Map();
|
|
432
|
+
idToPath = new Map();
|
|
433
|
+
nextId = 1;
|
|
434
|
+
register(dirPath) {
|
|
435
|
+
let id = this.pathToId.get(dirPath);
|
|
436
|
+
if (id != null)
|
|
437
|
+
return id;
|
|
438
|
+
id = this.nextId++;
|
|
439
|
+
this.pathToId.set(dirPath, id);
|
|
440
|
+
this.idToPath.set(id, dirPath);
|
|
441
|
+
if (this.pathToId.size > 500) {
|
|
442
|
+
const oldest = [...this.pathToId.entries()].slice(0, 200);
|
|
443
|
+
for (const [oldPath, oldId] of oldest) {
|
|
444
|
+
this.pathToId.delete(oldPath);
|
|
445
|
+
this.idToPath.delete(oldId);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return id;
|
|
449
|
+
}
|
|
450
|
+
resolve(id) {
|
|
451
|
+
return this.idToPath.get(id);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const feishuPathRegistry = new PathRegistry();
|
|
455
|
+
const DIR_PAGE_SIZE = 8;
|
|
456
|
+
export function resolveFeishuRegisteredPath(id) {
|
|
457
|
+
return feishuPathRegistry.resolve(id);
|
|
458
|
+
}
|
|
459
|
+
export function buildSwitchWorkdirCard(currentWorkdir, browsePath, page = 0) {
|
|
460
|
+
const dirs = listSubdirs(browsePath);
|
|
461
|
+
const totalPages = Math.max(1, Math.ceil(dirs.length / DIR_PAGE_SIZE));
|
|
462
|
+
const currentPage = Math.min(Math.max(0, page), totalPages - 1);
|
|
463
|
+
const slice = dirs.slice(currentPage * DIR_PAGE_SIZE, (currentPage + 1) * DIR_PAGE_SIZE);
|
|
464
|
+
// Text
|
|
465
|
+
const lines = ['**Workdir**'];
|
|
466
|
+
lines.push(`● \`${currentWorkdir}\``);
|
|
467
|
+
if (browsePath !== currentWorkdir)
|
|
468
|
+
lines.push(`○ \`${browsePath}\``);
|
|
469
|
+
// Directory buttons (2 per row)
|
|
470
|
+
const dirRows = [];
|
|
471
|
+
for (let i = 0; i < slice.length; i += 2) {
|
|
472
|
+
const rowActions = [];
|
|
473
|
+
for (let j = i; j < Math.min(i + 2, slice.length); j++) {
|
|
474
|
+
const fullPath = path.join(browsePath, slice[j]);
|
|
475
|
+
const id = feishuPathRegistry.register(fullPath);
|
|
476
|
+
rowActions.push(cardButton(slice[j], `sw:n:${id}:0`));
|
|
477
|
+
}
|
|
478
|
+
dirRows.push({ actions: rowActions });
|
|
479
|
+
}
|
|
480
|
+
// Nav row: parent + pagination
|
|
481
|
+
const navActions = [];
|
|
482
|
+
const parent = path.dirname(browsePath);
|
|
483
|
+
if (parent !== browsePath) {
|
|
484
|
+
navActions.push(cardButton('⬆ ..', `sw:n:${feishuPathRegistry.register(parent)}:0`));
|
|
485
|
+
}
|
|
486
|
+
if (totalPages > 1) {
|
|
487
|
+
const browseId = feishuPathRegistry.register(browsePath);
|
|
488
|
+
if (currentPage > 0)
|
|
489
|
+
navActions.push(cardButton(`◀ ${currentPage}/${totalPages}`, `sw:n:${browseId}:${currentPage - 1}`));
|
|
490
|
+
if (currentPage < totalPages - 1)
|
|
491
|
+
navActions.push(cardButton(`${currentPage + 2}/${totalPages} ▶`, `sw:n:${browseId}:${currentPage + 1}`));
|
|
492
|
+
}
|
|
493
|
+
// Select button
|
|
494
|
+
const selectActions = [
|
|
495
|
+
cardButton('✓ Use This', `sw:s:${feishuPathRegistry.register(browsePath)}`, true),
|
|
496
|
+
];
|
|
497
|
+
const rows = [
|
|
498
|
+
...dirRows,
|
|
499
|
+
...(navActions.length ? [{ actions: navActions }] : []),
|
|
500
|
+
{ actions: selectActions },
|
|
501
|
+
];
|
|
502
|
+
return { markdown: lines.join('\n'), rows };
|
|
503
|
+
}
|
|
504
|
+
export function renderHost(d) {
|
|
505
|
+
const lines = [
|
|
506
|
+
'**Host**',
|
|
507
|
+
'',
|
|
508
|
+
`**Name:** ${d.hostName}`,
|
|
509
|
+
`**CPU:** ${d.cpuModel} x${d.cpuCount}`,
|
|
510
|
+
d.cpuUsage
|
|
511
|
+
? `**CPU Usage:** ${d.cpuUsage.usedPercent.toFixed(1)}% (${d.cpuUsage.userPercent.toFixed(1)}% user, ${d.cpuUsage.sysPercent.toFixed(1)}% sys, ${d.cpuUsage.idlePercent.toFixed(1)}% idle)`
|
|
512
|
+
: '**CPU Usage:** unavailable',
|
|
513
|
+
`**Memory:** ${fmtBytes(d.memoryUsed)} / ${fmtBytes(d.totalMem)} (${d.memoryPercent.toFixed(0)}%)`,
|
|
514
|
+
`**Available:** ${fmtBytes(d.memoryAvailable)}`,
|
|
515
|
+
`**Battery:** ${d.battery ? `${d.battery.percent} (${d.battery.state})` : 'unavailable'}`,
|
|
516
|
+
];
|
|
517
|
+
if (d.disk)
|
|
518
|
+
lines.push(`**Disk:** ${d.disk.used} used / ${d.disk.total} total (${d.disk.percent})`);
|
|
519
|
+
lines.push(`\n**Process:** PID ${d.selfPid} | RSS ${fmtBytes(d.selfRss)} | Heap ${fmtBytes(d.selfHeap)}`);
|
|
520
|
+
if (d.topProcs.length > 1) {
|
|
521
|
+
lines.push('\n**Top Processes**');
|
|
522
|
+
lines.push('```');
|
|
523
|
+
lines.push(...d.topProcs);
|
|
524
|
+
lines.push('```');
|
|
525
|
+
}
|
|
526
|
+
return lines.join('\n');
|
|
527
|
+
}
|