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,192 @@
1
+ /**
2
+ * Host system data collection: battery, CPU, memory, display name.
3
+ * Platform-adaptive with macOS, Linux, and Windows support.
4
+ */
5
+ import os from 'node:os';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { execSync } from 'node:child_process';
9
+ function normalizeBatteryState(raw) {
10
+ const state = (raw || '').trim().toLowerCase().replace(/\s+/g, ' ');
11
+ if (!state)
12
+ return 'unknown';
13
+ if (state === 'finishing charge')
14
+ return 'charging';
15
+ if (state === 'ac attached')
16
+ return 'plugged in';
17
+ return state;
18
+ }
19
+ function getMacBatteryData() {
20
+ try {
21
+ const output = execSync('pmset -g batt', { encoding: 'utf-8', timeout: 3000 }).trim();
22
+ if (!output || /no batteries/i.test(output))
23
+ return null;
24
+ const line = output.split('\n').find(v => /\d+%/.test(v));
25
+ if (!line)
26
+ return null;
27
+ const percent = line.match(/(\d+)%/)?.[1];
28
+ if (!percent)
29
+ return null;
30
+ const states = line
31
+ .split(';')
32
+ .slice(1)
33
+ .map(segment => segment.replace(/\bpresent:\s*(true|false)\b/ig, '').trim())
34
+ .filter(Boolean);
35
+ const state = states.find(segment => /(charging|discharging|charged|not charging|finishing charge|full)/i.test(segment))
36
+ ?? states.find(segment => !/remaining/i.test(segment))
37
+ ?? 'unknown';
38
+ return { percent: `${percent}%`, state: normalizeBatteryState(state) };
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ function getLinuxBatteryData() {
45
+ try {
46
+ const powerDir = '/sys/class/power_supply';
47
+ const batteries = fs.readdirSync(powerDir).filter(name => /^BAT/i.test(name));
48
+ for (const battery of batteries) {
49
+ const batteryDir = path.join(powerDir, battery);
50
+ const capacityPath = path.join(batteryDir, 'capacity');
51
+ if (!fs.existsSync(capacityPath))
52
+ continue;
53
+ const capacity = fs.readFileSync(capacityPath, 'utf-8').trim();
54
+ if (!capacity)
55
+ continue;
56
+ const statusPath = path.join(batteryDir, 'status');
57
+ const state = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf-8').trim() : 'unknown';
58
+ return {
59
+ percent: capacity.endsWith('%') ? capacity : `${capacity}%`,
60
+ state: normalizeBatteryState(state),
61
+ };
62
+ }
63
+ }
64
+ catch { }
65
+ try {
66
+ const output = execSync('upower -e | grep -m1 battery | xargs -I{} upower -i "{}"', { encoding: 'utf-8', timeout: 3000 }).trim();
67
+ if (!output)
68
+ return null;
69
+ const percent = output.match(/percentage:\s*(\d+%)/i)?.[1];
70
+ if (!percent)
71
+ return null;
72
+ const state = output.match(/state:\s*([^\n]+)/i)?.[1];
73
+ return { percent, state: normalizeBatteryState(state) };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function getWindowsBatteryData() {
80
+ try {
81
+ const output = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Battery | Select-Object -First 1 EstimatedChargeRemaining,BatteryStatus | ConvertTo-Json -Compress"', { encoding: 'utf-8', timeout: 3000 }).trim();
82
+ if (!output || output === 'null')
83
+ return null;
84
+ const parsed = JSON.parse(output);
85
+ const percent = Number(parsed?.EstimatedChargeRemaining);
86
+ if (!Number.isFinite(percent))
87
+ return null;
88
+ const status = Number(parsed?.BatteryStatus);
89
+ const state = status === 6 ? 'charging'
90
+ : status === 3 ? 'charged'
91
+ : status === 2 ? 'plugged in'
92
+ : status === 1 ? 'discharging'
93
+ : 'unknown';
94
+ return { percent: `${percent}%`, state };
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ export function getHostBatteryData() {
101
+ if (process.platform === 'darwin')
102
+ return getMacBatteryData();
103
+ if (process.platform === 'linux')
104
+ return getLinuxBatteryData();
105
+ if (process.platform === 'win32')
106
+ return getWindowsBatteryData();
107
+ return null;
108
+ }
109
+ function parsePercent(value) {
110
+ if (!value)
111
+ return null;
112
+ const n = Number.parseFloat(value.trim());
113
+ return Number.isFinite(n) ? n : null;
114
+ }
115
+ function getMacCpuUsageData() {
116
+ try {
117
+ const output = execSync('top -l 1 -n 0 | sed -n \'1,6p\'', { encoding: 'utf-8', timeout: 3000 });
118
+ const line = output.split('\n').find(entry => /^CPU usage:/i.test(entry.trim()));
119
+ if (!line)
120
+ return null;
121
+ const match = line.match(/CPU usage:\s*([\d.]+)% user,\s*([\d.]+)% sys,\s*([\d.]+)% idle/i);
122
+ if (!match)
123
+ return null;
124
+ const userPercent = parsePercent(match[1]);
125
+ const sysPercent = parsePercent(match[2]);
126
+ const idlePercent = parsePercent(match[3]);
127
+ if (userPercent == null || sysPercent == null || idlePercent == null)
128
+ return null;
129
+ return {
130
+ userPercent,
131
+ sysPercent,
132
+ idlePercent,
133
+ usedPercent: Math.max(0, userPercent + sysPercent),
134
+ };
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function getMacMemoryUsageData(totalMem) {
141
+ try {
142
+ const output = execSync('vm_stat', { encoding: 'utf-8', timeout: 3000 });
143
+ const pageSize = Number.parseInt(output.match(/page size of (\d+) bytes/i)?.[1] || '', 10);
144
+ if (!Number.isFinite(pageSize) || pageSize <= 0)
145
+ return null;
146
+ const pages = new Map();
147
+ for (const line of output.split('\n')) {
148
+ const match = line.match(/^Pages ([^:]+):\s+(\d+)\./);
149
+ if (!match)
150
+ continue;
151
+ pages.set(match[1].trim().toLowerCase(), Number.parseInt(match[2], 10));
152
+ }
153
+ const reclaimablePages = (pages.get('free') || 0) +
154
+ (pages.get('inactive') || 0) +
155
+ (pages.get('speculative') || 0) +
156
+ (pages.get('purgeable') || 0);
157
+ const availableBytes = Math.max(0, reclaimablePages * pageSize);
158
+ const usedBytes = Math.max(0, Math.min(totalMem, totalMem - availableBytes));
159
+ const percent = totalMem > 0 ? (usedBytes / totalMem) * 100 : 0;
160
+ return { usedBytes, availableBytes, percent, source: 'vm_stat' };
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ export function getHostCpuUsageData() {
167
+ if (process.platform === 'darwin')
168
+ return getMacCpuUsageData();
169
+ return null;
170
+ }
171
+ export function getHostDisplayName() {
172
+ if (process.platform === 'darwin') {
173
+ try {
174
+ const name = execSync('scutil --get ComputerName', { encoding: 'utf-8', timeout: 3000 }).trim();
175
+ if (name)
176
+ return name;
177
+ }
178
+ catch { /* fall through */ }
179
+ }
180
+ return os.hostname();
181
+ }
182
+ export function getHostMemoryUsageData(totalMem, freeMem) {
183
+ if (process.platform === 'darwin') {
184
+ const macData = getMacMemoryUsageData(totalMem);
185
+ if (macData)
186
+ return macData;
187
+ }
188
+ const usedBytes = Math.max(0, totalMem - freeMem);
189
+ const availableBytes = Math.max(0, freeMem);
190
+ const percent = totalMem > 0 ? (usedBytes / totalMem) * 100 : 0;
191
+ return { usedBytes, availableBytes, percent, source: 'os' };
192
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Human-in-the-loop prompt state machine and answer helpers.
3
+ */
4
+ export function createEmptyHumanLoopAnswer() {
5
+ return {
6
+ selectedValue: null,
7
+ freeformText: null,
8
+ awaitingFreeform: false,
9
+ skipped: false,
10
+ };
11
+ }
12
+ export function currentHumanLoopQuestion(prompt) {
13
+ return prompt.questions[prompt.currentIndex] || null;
14
+ }
15
+ export function isHumanLoopQuestionAnswered(prompt, questionIndex) {
16
+ const question = prompt.questions[questionIndex];
17
+ if (!question)
18
+ return false;
19
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
20
+ if (answer.awaitingFreeform)
21
+ return false;
22
+ if (answer.skipped)
23
+ return true;
24
+ if (answer.selectedValue)
25
+ return true;
26
+ if (answer.freeformText && answer.freeformText.trim())
27
+ return true;
28
+ return false;
29
+ }
30
+ export function humanLoopAnsweredCount(prompt) {
31
+ let count = 0;
32
+ for (let i = 0; i < prompt.questions.length; i++) {
33
+ if (isHumanLoopQuestionAnswered(prompt, i))
34
+ count++;
35
+ }
36
+ return count;
37
+ }
38
+ export function isHumanLoopAwaitingText(prompt) {
39
+ const question = currentHumanLoopQuestion(prompt);
40
+ if (!question)
41
+ return false;
42
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
43
+ if (answer.awaitingFreeform)
44
+ return true;
45
+ const hasOptions = !!question.options?.length;
46
+ return !hasOptions;
47
+ }
48
+ export function humanLoopOptionSelected(prompt, optionValue) {
49
+ const question = currentHumanLoopQuestion(prompt);
50
+ if (!question)
51
+ return false;
52
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
53
+ return answer.selectedValue === optionValue && !answer.awaitingFreeform;
54
+ }
55
+ export function setHumanLoopOption(prompt, optionValue, opts = {}) {
56
+ const question = currentHumanLoopQuestion(prompt);
57
+ if (!question)
58
+ return { completed: false, advanced: false };
59
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
60
+ answer.selectedValue = opts.requestFreeform ? null : optionValue;
61
+ answer.awaitingFreeform = !!opts.requestFreeform;
62
+ answer.skipped = false;
63
+ if (!opts.requestFreeform)
64
+ answer.freeformText = null;
65
+ prompt.answers[question.id] = answer;
66
+ if (opts.requestFreeform)
67
+ return { completed: false, advanced: false };
68
+ return advanceHumanLoopPrompt(prompt);
69
+ }
70
+ export function setHumanLoopText(prompt, text) {
71
+ const question = currentHumanLoopQuestion(prompt);
72
+ if (!question)
73
+ return { completed: false, advanced: false };
74
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
75
+ answer.freeformText = text;
76
+ answer.awaitingFreeform = false;
77
+ answer.skipped = false;
78
+ prompt.answers[question.id] = answer;
79
+ return advanceHumanLoopPrompt(prompt);
80
+ }
81
+ export function skipHumanLoopQuestion(prompt) {
82
+ const question = currentHumanLoopQuestion(prompt);
83
+ if (!question)
84
+ return { completed: false, advanced: false };
85
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
86
+ answer.freeformText = null;
87
+ answer.selectedValue = null;
88
+ answer.awaitingFreeform = false;
89
+ answer.skipped = true;
90
+ prompt.answers[question.id] = answer;
91
+ return advanceHumanLoopPrompt(prompt);
92
+ }
93
+ function advanceHumanLoopPrompt(prompt) {
94
+ if (prompt.currentIndex >= prompt.questions.length - 1)
95
+ return { completed: true, advanced: false };
96
+ prompt.currentIndex += 1;
97
+ return { completed: false, advanced: true };
98
+ }
99
+ export function summarizeHumanLoopAnswer(prompt, question) {
100
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
101
+ const values = [];
102
+ if (answer.selectedValue)
103
+ values.push(answer.selectedValue);
104
+ const freeform = answer.freeformText?.trim();
105
+ if (freeform)
106
+ values.push(freeform);
107
+ const display = answer.skipped
108
+ ? '(skip)'
109
+ : values.length
110
+ ? (question.secret ? '(hidden)' : values.join(' | '))
111
+ : '(pending)';
112
+ return {
113
+ values,
114
+ display,
115
+ };
116
+ }
117
+ export function buildHumanLoopResponse(prompt) {
118
+ const answers = {};
119
+ for (const question of prompt.questions) {
120
+ answers[question.id] = summarizeHumanLoopAnswer(prompt, question).values;
121
+ }
122
+ return prompt.resolveWith(answers);
123
+ }
124
+ function displayValueForOption(question, value) {
125
+ const match = question.options?.find(opt => opt.value === value);
126
+ return match?.label || value;
127
+ }
128
+ /**
129
+ * Build a channel-agnostic summary of a prompt's resolved answers. Used by the
130
+ * base bot's `onInteractionAnswered` hook so each channel renders the same
131
+ * closed-state view + echo message without diverging.
132
+ */
133
+ export function summarizeResolvedHumanLoopAnswers(prompt, status = 'answered') {
134
+ const rows = [];
135
+ const compactParts = [];
136
+ for (const question of prompt.questions) {
137
+ const answer = prompt.answers[question.id] || createEmptyHumanLoopAnswer();
138
+ let display;
139
+ if (answer.skipped) {
140
+ display = '(skip)';
141
+ }
142
+ else if (question.secret && (answer.selectedValue || answer.freeformText)) {
143
+ display = '(hidden)';
144
+ }
145
+ else {
146
+ const parts = [];
147
+ if (answer.selectedValue)
148
+ parts.push(displayValueForOption(question, answer.selectedValue));
149
+ const freeform = answer.freeformText?.trim();
150
+ if (freeform)
151
+ parts.push(freeform);
152
+ display = parts.length ? parts.join(' · ') : '(no answer)';
153
+ }
154
+ const label = (question.header || question.prompt || question.id).trim();
155
+ rows.push({
156
+ label,
157
+ display,
158
+ skipped: !!answer.skipped,
159
+ secret: !!question.secret,
160
+ });
161
+ compactParts.push(display);
162
+ }
163
+ return {
164
+ status,
165
+ rows,
166
+ display: compactParts.join(' · '),
167
+ };
168
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Menu command definitions and skill-to-command mapping.
3
+ */
4
+ export const SKILL_CMD_PREFIX = 'sk_';
5
+ export function buildWelcomeIntro(version) {
6
+ return {
7
+ title: "Hi, I'm pikiloop",
8
+ subtitle: 'Send me a message to get started.',
9
+ version,
10
+ };
11
+ }
12
+ export function buildSkillCommandName(skillName) {
13
+ const normalized = skillName.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
14
+ if (!normalized)
15
+ return null;
16
+ const cmdName = `${SKILL_CMD_PREFIX}${normalized}`;
17
+ if (cmdName.length > 32)
18
+ return null;
19
+ return cmdName;
20
+ }
21
+ export function indexSkillsByCommand(skills) {
22
+ const indexed = new Map();
23
+ for (const skill of skills) {
24
+ const cmdName = buildSkillCommandName(skill.name);
25
+ if (!cmdName || indexed.has(cmdName))
26
+ continue;
27
+ indexed.set(cmdName, skill);
28
+ }
29
+ return indexed;
30
+ }
31
+ export function buildDefaultMenuCommands(agentCount, skills = []) {
32
+ const commands = [
33
+ { command: 'sessions', description: 'Switch sessions' },
34
+ ];
35
+ if (agentCount > 1) {
36
+ commands.push({ command: 'agents', description: 'Switch agents' });
37
+ }
38
+ commands.push({ command: 'switch', description: 'Change workdir' }, { command: 'workspaces', description: 'Pick saved workspace' }, { command: 'models', description: 'Switch models' }, { command: 'mode', description: 'Toggle plan mode' }, { command: 'goal', description: 'Set/inspect a persistent goal' }, { command: 'stop', description: 'Stop current session' }, { command: 'status', description: 'Show status' }, { command: 'host', description: 'Host info' });
39
+ if (skills.length) {
40
+ commands.push({ command: 'skills', description: 'Browse skills' });
41
+ }
42
+ commands.push({ command: 'ext', description: 'Extensions overview' });
43
+ if (agentCount === 1) {
44
+ commands.push({ command: 'agents', description: 'Switch agents' });
45
+ }
46
+ commands.push({ command: 'restart', description: 'Restart bot' });
47
+ return commands;
48
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Session and message orchestration helpers shared across channels.
3
+ */
4
+ import { buildDefaultMenuCommands } from './menu.js';
5
+ import { BOT_SHUTDOWN_FORCE_EXIT_MS as _BOT_SHUTDOWN_FORCE_EXIT_MS } from '../core/constants.js';
6
+ export const BOT_SHUTDOWN_FORCE_EXIT_MS = _BOT_SHUTDOWN_FORCE_EXIT_MS;
7
+ export function buildBotMenuState(bot) {
8
+ const agents = bot.fetchAgents().agents;
9
+ const installedCount = agents.filter(agent => agent.installed).length;
10
+ const skills = bot.fetchSkills().skills;
11
+ return {
12
+ commands: buildDefaultMenuCommands(installedCount, skills),
13
+ skillCount: skills.length,
14
+ };
15
+ }
16
+ export function buildSessionTaskId(session, seq, now = Date.now()) {
17
+ return `${session.key}:${now.toString(36)}:${seq.toString(36)}`;
18
+ }
19
+ export function buildKnownChatEnv(allowedChatIds, knownChatIds, envName) {
20
+ const ids = new Set();
21
+ for (const id of allowedChatIds)
22
+ ids.add(String(id));
23
+ for (const id of knownChatIds)
24
+ ids.add(String(id));
25
+ return ids.size ? { [envName]: [...ids].join(',') } : {};
26
+ }
27
+ export class SessionMessageRegistry {
28
+ maxPerChat;
29
+ messages = new Map();
30
+ constructor(maxPerChat = 1024) {
31
+ this.maxPerChat = maxPerChat;
32
+ }
33
+ clear() {
34
+ this.messages.clear();
35
+ }
36
+ register(chatId, messageId, session, workdir) {
37
+ if (session.workdir !== workdir)
38
+ return;
39
+ if (!this.isValidMessageId(messageId))
40
+ return;
41
+ let chatMessages = this.messages.get(chatId);
42
+ if (!chatMessages) {
43
+ chatMessages = new Map();
44
+ this.messages.set(chatId, chatMessages);
45
+ }
46
+ chatMessages.set(messageId, {
47
+ key: session.key,
48
+ workdir: session.workdir,
49
+ agent: session.agent,
50
+ sessionId: session.sessionId,
51
+ workspacePath: session.workspacePath ?? null,
52
+ threadId: session.threadId ?? null,
53
+ codexCumulative: session.codexCumulative,
54
+ modelId: session.modelId ?? null,
55
+ });
56
+ while (chatMessages.size > this.maxPerChat) {
57
+ const oldest = chatMessages.keys().next();
58
+ if (oldest.done)
59
+ break;
60
+ chatMessages.delete(oldest.value);
61
+ }
62
+ }
63
+ registerMany(chatId, messageIds, session, workdir) {
64
+ for (const messageId of messageIds)
65
+ this.register(chatId, messageId, session, workdir);
66
+ }
67
+ resolve(chatId, messageId) {
68
+ if (!this.isValidMessageId(messageId))
69
+ return null;
70
+ return this.messages.get(chatId)?.get(messageId) || null;
71
+ }
72
+ isValidMessageId(messageId) {
73
+ if (typeof messageId === 'number')
74
+ return Number.isFinite(messageId);
75
+ if (typeof messageId === 'string')
76
+ return messageId.length > 0;
77
+ return false;
78
+ }
79
+ }