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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * bot-commands.ts — channel-agnostic command data layer.
3
+ *
4
+ * Each function returns structured data objects that any IM renderer can consume.
5
+ * No rendering, no HTML, no platform-specific formatting.
6
+ *
7
+ * Usage from a channel-specific bot (e.g. bot-telegram.ts, bot-feishu.ts):
8
+ * const data = await getSessionsPageData(bot, chatId, 0);
9
+ * const rendered = renderSessionsPage(data); // channel-specific renderer
10
+ */
11
+ import path from 'node:path';
12
+ import { VERSION, fmtTokens, fmtUptime, fmtBytes } from './bot.js';
13
+ import { getProjectSkillPaths } from './code-agent.js';
14
+ import { getDriver } from './agent-driver.js';
15
+ import { buildWelcomeIntro, buildDefaultMenuCommands, buildSkillCommandName, indexSkillsByCommand, SKILL_CMD_PREFIX } from './bot-menu.js';
16
+ import { summarizePromptForStatus } from './bot-streaming.js';
17
+ import { getSessionStatusForChat } from './session-status.js';
18
+ export function getStartData(bot, chatId) {
19
+ const cs = bot.chat(chatId);
20
+ const intro = buildWelcomeIntro(VERSION);
21
+ const res = bot.fetchAgents();
22
+ const installedCount = res.agents.filter(a => a.installed).length;
23
+ const skillRes = bot.fetchSkills();
24
+ const commands = buildDefaultMenuCommands(installedCount, skillRes.skills);
25
+ return {
26
+ ...intro,
27
+ agent: cs.agent,
28
+ workdir: bot.workdir,
29
+ commands,
30
+ };
31
+ }
32
+ export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
33
+ const cs = bot.chat(chatId);
34
+ const res = await bot.fetchSessions(cs.agent);
35
+ const sessions = res.ok ? res.sessions : [];
36
+ const total = sessions.length;
37
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
38
+ const pg = Math.max(0, Math.min(page, totalPages - 1));
39
+ const slice = sessions.slice(pg * pageSize, (pg + 1) * pageSize);
40
+ const entries = [];
41
+ for (const s of slice) {
42
+ const sessionKey = s.sessionId || '';
43
+ if (!sessionKey)
44
+ continue;
45
+ const status = getSessionStatusForChat(bot, cs, s);
46
+ const title = s.title ? s.title.replace(/\n/g, ' ').slice(0, 10) : sessionKey.slice(0, 10);
47
+ const time = s.createdAt
48
+ ? new Date(s.createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
49
+ : '?';
50
+ entries.push({ key: sessionKey, title, time, isCurrent: status.isCurrent, isRunning: status.isRunning });
51
+ }
52
+ return { agent: cs.agent, total, page: pg, totalPages, sessions: entries };
53
+ }
54
+ export function extractLastSessionTurn(messages) {
55
+ if (!messages.length)
56
+ return null;
57
+ let lastUserIndex = -1;
58
+ for (let i = messages.length - 1; i >= 0; i--) {
59
+ if (messages[i].role === 'user') {
60
+ lastUserIndex = i;
61
+ break;
62
+ }
63
+ }
64
+ const userText = String(lastUserIndex >= 0 ? messages[lastUserIndex].text : '').trim() || null;
65
+ const assistantTexts = [];
66
+ for (let i = lastUserIndex >= 0 ? lastUserIndex + 1 : 0; i < messages.length; i++) {
67
+ if (messages[i].role === 'assistant' && messages[i].text)
68
+ assistantTexts.push(messages[i].text);
69
+ }
70
+ const assistantText = assistantTexts.join('\n\n').trim() || null;
71
+ if (!userText && !assistantText)
72
+ return null;
73
+ return { userText, assistantText };
74
+ }
75
+ export async function getSessionTurnPreviewData(bot, agent, sessionId, limit = 50) {
76
+ if (!sessionId)
77
+ return null;
78
+ const tail = await bot.fetchSessionTail(agent, sessionId, limit);
79
+ if (!tail.ok || !tail.messages.length)
80
+ return null;
81
+ return extractLastSessionTurn(tail.messages);
82
+ }
83
+ export function getAgentsListData(bot, chatId) {
84
+ const cs = bot.chat(chatId);
85
+ const res = bot.fetchAgents();
86
+ return {
87
+ currentAgent: cs.agent,
88
+ agents: res.agents.map(a => ({
89
+ agent: a.agent,
90
+ installed: a.installed,
91
+ version: a.version ?? null,
92
+ path: a.path ?? null,
93
+ isCurrent: a.agent === cs.agent,
94
+ })),
95
+ };
96
+ }
97
+ export function getSkillsListData(bot, chatId) {
98
+ const cs = bot.chat(chatId);
99
+ const skills = bot.fetchSkills().skills
100
+ .map(skill => {
101
+ const command = buildSkillCommandName(skill.name);
102
+ if (!command)
103
+ return null;
104
+ return {
105
+ name: skill.name,
106
+ label: skill.label || skill.name.charAt(0).toUpperCase() + skill.name.slice(1),
107
+ description: skill.description,
108
+ command,
109
+ source: skill.source,
110
+ };
111
+ })
112
+ .filter((skill) => !!skill);
113
+ return { agent: cs.agent, workdir: bot.workdir, skills };
114
+ }
115
+ function claudeModelSelectionKey(modelId) {
116
+ const value = String(modelId || '').trim().toLowerCase();
117
+ if (!value)
118
+ return null;
119
+ if (value === 'opus' || value === 'opus-1m' || value.startsWith('claude-opus-')) {
120
+ return value === 'opus-1m' || value.endsWith('[1m]') ? 'opus-1m' : 'opus';
121
+ }
122
+ if (value === 'sonnet' || value === 'sonnet-1m' || value.startsWith('claude-sonnet-')) {
123
+ return value === 'sonnet-1m' || value.endsWith('[1m]') ? 'sonnet-1m' : 'sonnet';
124
+ }
125
+ if (value === 'haiku' || value.startsWith('claude-haiku-'))
126
+ return 'haiku';
127
+ return null;
128
+ }
129
+ export function modelMatchesSelection(agent, selection, currentModel) {
130
+ if (selection === currentModel)
131
+ return true;
132
+ if (agent !== 'claude')
133
+ return false;
134
+ const a = claudeModelSelectionKey(selection);
135
+ const b = claudeModelSelectionKey(currentModel);
136
+ return !!a && a === b;
137
+ }
138
+ const EFFORT_LEVELS = {
139
+ claude: [
140
+ { id: 'low', label: 'Low' },
141
+ { id: 'medium', label: 'Medium' },
142
+ { id: 'high', label: 'High' },
143
+ ],
144
+ codex: [
145
+ { id: 'low', label: 'Low' },
146
+ { id: 'medium', label: 'Medium' },
147
+ { id: 'high', label: 'High' },
148
+ { id: 'xhigh', label: 'Very High' },
149
+ ],
150
+ };
151
+ function buildEffortData(bot, agent) {
152
+ const currentEffort = bot.effortForAgent(agent);
153
+ if (!currentEffort)
154
+ return null;
155
+ const levels = EFFORT_LEVELS[agent];
156
+ if (!levels)
157
+ return null;
158
+ return {
159
+ current: currentEffort,
160
+ levels: levels.map(l => ({ ...l, isCurrent: l.id === currentEffort })),
161
+ };
162
+ }
163
+ export async function getModelsListData(bot, chatId) {
164
+ const cs = bot.chat(chatId);
165
+ const currentModel = bot.modelForAgent(cs.agent);
166
+ const res = await bot.fetchModels(cs.agent);
167
+ return {
168
+ agent: cs.agent,
169
+ currentModel,
170
+ sources: res.sources,
171
+ note: res.note ?? null,
172
+ models: res.models.map(m => ({
173
+ id: m.id,
174
+ alias: m.alias ?? null,
175
+ isCurrent: modelMatchesSelection(cs.agent, m.id, currentModel),
176
+ })),
177
+ effort: buildEffortData(bot, cs.agent),
178
+ };
179
+ }
180
+ export async function getStatusDataAsync(bot, chatId) {
181
+ const d = bot.getStatusData(chatId);
182
+ const driver = getDriver(d.agent);
183
+ const usage = driver.getUsageLive
184
+ ? await driver.getUsageLive({ agent: d.agent, model: d.model }).catch(() => d.usage)
185
+ : d.usage;
186
+ return {
187
+ ...d,
188
+ running: d.running ? { prompt: d.running.prompt, startedAt: d.running.startedAt } : null,
189
+ usage,
190
+ };
191
+ }
192
+ export function getHostDataSync(bot) {
193
+ return bot.getHostData();
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Skill routing
197
+ // ---------------------------------------------------------------------------
198
+ export { SKILL_CMD_PREFIX, indexSkillsByCommand };
199
+ function relSkillPath(workdir, filePath) {
200
+ const relative = path.relative(workdir, filePath).replace(/\\/g, '/');
201
+ return relative && !relative.startsWith('..') ? relative : filePath;
202
+ }
203
+ export function resolveSkillPrompt(bot, chatId, cmd, args) {
204
+ const skills = bot.fetchSkills().skills;
205
+ const skill = indexSkillsByCommand(skills).get(cmd);
206
+ if (!skill)
207
+ return null;
208
+ const cs = bot.chat(chatId);
209
+ const extra = args.trim();
210
+ const suffix = extra ? ` Additional context: ${extra}` : '';
211
+ let prompt;
212
+ if (skill.source === 'commands') {
213
+ prompt = `In this project's .claude/commands/${skill.name}.md file, there is a custom command definition. Please read and execute the instructions defined there.${suffix}`;
214
+ return { prompt, skillName: skill.name };
215
+ }
216
+ const paths = getProjectSkillPaths(bot.workdir, skill.name);
217
+ if (cs.agent === 'claude' && paths.claudeSkillFile) {
218
+ prompt = `Please execute the /${skill.name} skill defined in this project.${suffix}`;
219
+ }
220
+ else {
221
+ const canonicalPath = paths.sharedSkillFile
222
+ ? `\`${relSkillPath(bot.workdir, paths.sharedSkillFile)}\``
223
+ : `\`.pikiclaw/skills/${skill.name}/SKILL.md\``;
224
+ const locationText = paths.sharedSkillFile
225
+ ? canonicalPath
226
+ : paths.agentsSkillFile || paths.claudeSkillFile
227
+ ? canonicalPath
228
+ : `\`${skill.name}/SKILL.md\``;
229
+ prompt = `In this project, the ${skill.name} skill is defined in ${locationText}. Please read that SKILL.md file and execute the instructions.${suffix}`;
230
+ }
231
+ return { prompt, skillName: skill.name };
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // Re-export commonly used helpers for convenience
235
+ // ---------------------------------------------------------------------------
236
+ export { summarizePromptForStatus, fmtTokens, fmtUptime, fmtBytes, VERSION };