skimpyclaw 0.1.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 (219) hide show
  1. package/README.md +230 -0
  2. package/dist/__tests__/agent.test.d.ts +1 -0
  3. package/dist/__tests__/agent.test.js +131 -0
  4. package/dist/__tests__/api.test.d.ts +1 -0
  5. package/dist/__tests__/api.test.js +1227 -0
  6. package/dist/__tests__/audit.test.d.ts +1 -0
  7. package/dist/__tests__/audit.test.js +122 -0
  8. package/dist/__tests__/cache.test.d.ts +1 -0
  9. package/dist/__tests__/cache.test.js +65 -0
  10. package/dist/__tests__/channels.test.d.ts +1 -0
  11. package/dist/__tests__/channels.test.js +85 -0
  12. package/dist/__tests__/cli.integration.test.d.ts +1 -0
  13. package/dist/__tests__/cli.integration.test.js +16 -0
  14. package/dist/__tests__/cli.test.d.ts +1 -0
  15. package/dist/__tests__/cli.test.js +230 -0
  16. package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
  17. package/dist/__tests__/code-agents-executor.test.js +75 -0
  18. package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
  19. package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
  20. package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
  21. package/dist/__tests__/code-agents-parser.test.js +39 -0
  22. package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
  23. package/dist/__tests__/code-agents-utils.test.js +41 -0
  24. package/dist/__tests__/config.test.d.ts +1 -0
  25. package/dist/__tests__/config.test.js +46 -0
  26. package/dist/__tests__/cron.test.d.ts +1 -0
  27. package/dist/__tests__/cron.test.js +66 -0
  28. package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
  29. package/dist/__tests__/dashboard-mode.test.js +145 -0
  30. package/dist/__tests__/dashboard.test.d.ts +1 -0
  31. package/dist/__tests__/dashboard.test.js +43 -0
  32. package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
  33. package/dist/__tests__/doctor.formatters.test.js +65 -0
  34. package/dist/__tests__/doctor.index.test.d.ts +1 -0
  35. package/dist/__tests__/doctor.index.test.js +48 -0
  36. package/dist/__tests__/doctor.runner.test.d.ts +1 -0
  37. package/dist/__tests__/doctor.runner.test.js +204 -0
  38. package/dist/__tests__/exec-approval.test.d.ts +1 -0
  39. package/dist/__tests__/exec-approval.test.js +323 -0
  40. package/dist/__tests__/file-lock.test.d.ts +1 -0
  41. package/dist/__tests__/file-lock.test.js +92 -0
  42. package/dist/__tests__/langfuse.test.d.ts +1 -0
  43. package/dist/__tests__/langfuse.test.js +40 -0
  44. package/dist/__tests__/model-selection.test.d.ts +1 -0
  45. package/dist/__tests__/model-selection.test.js +62 -0
  46. package/dist/__tests__/orchestrator.test.d.ts +1 -0
  47. package/dist/__tests__/orchestrator.test.js +425 -0
  48. package/dist/__tests__/providers-init.test.d.ts +1 -0
  49. package/dist/__tests__/providers-init.test.js +32 -0
  50. package/dist/__tests__/providers-routing.test.d.ts +1 -0
  51. package/dist/__tests__/providers-routing.test.js +25 -0
  52. package/dist/__tests__/providers-utils.test.d.ts +1 -0
  53. package/dist/__tests__/providers-utils.test.js +54 -0
  54. package/dist/__tests__/security.test.d.ts +1 -0
  55. package/dist/__tests__/security.test.js +22 -0
  56. package/dist/__tests__/sessions.test.d.ts +1 -0
  57. package/dist/__tests__/sessions.test.js +147 -0
  58. package/dist/__tests__/setup.test.d.ts +1 -0
  59. package/dist/__tests__/setup.test.js +114 -0
  60. package/dist/__tests__/skills.test.d.ts +1 -0
  61. package/dist/__tests__/skills.test.js +333 -0
  62. package/dist/__tests__/subagent.test.d.ts +1 -0
  63. package/dist/__tests__/subagent.test.js +240 -0
  64. package/dist/__tests__/telegram-utils.test.d.ts +1 -0
  65. package/dist/__tests__/telegram-utils.test.js +22 -0
  66. package/dist/__tests__/telegram.test.d.ts +1 -0
  67. package/dist/__tests__/telegram.test.js +42 -0
  68. package/dist/__tests__/token-efficiency.test.d.ts +1 -0
  69. package/dist/__tests__/token-efficiency.test.js +38 -0
  70. package/dist/__tests__/tool-guard.test.d.ts +1 -0
  71. package/dist/__tests__/tool-guard.test.js +105 -0
  72. package/dist/__tests__/tools.test.d.ts +1 -0
  73. package/dist/__tests__/tools.test.js +589 -0
  74. package/dist/__tests__/usage.test.d.ts +1 -0
  75. package/dist/__tests__/usage.test.js +197 -0
  76. package/dist/__tests__/voice.test.d.ts +1 -0
  77. package/dist/__tests__/voice.test.js +214 -0
  78. package/dist/agent.d.ts +24 -0
  79. package/dist/agent.js +269 -0
  80. package/dist/api.d.ts +3 -0
  81. package/dist/api.js +943 -0
  82. package/dist/audit.d.ts +26 -0
  83. package/dist/audit.js +121 -0
  84. package/dist/cache.d.ts +8 -0
  85. package/dist/cache.js +24 -0
  86. package/dist/channels/telegram/handlers.d.ts +41 -0
  87. package/dist/channels/telegram/handlers.js +498 -0
  88. package/dist/channels/telegram/index.d.ts +14 -0
  89. package/dist/channels/telegram/index.js +326 -0
  90. package/dist/channels/telegram/types.d.ts +26 -0
  91. package/dist/channels/telegram/types.js +31 -0
  92. package/dist/channels/telegram/utils.d.ts +25 -0
  93. package/dist/channels/telegram/utils.js +256 -0
  94. package/dist/channels.d.ts +11 -0
  95. package/dist/channels.js +118 -0
  96. package/dist/cli.d.ts +5 -0
  97. package/dist/cli.js +768 -0
  98. package/dist/code-agents/executor.d.ts +5 -0
  99. package/dist/code-agents/executor.js +463 -0
  100. package/dist/code-agents/index.d.ts +22 -0
  101. package/dist/code-agents/index.js +199 -0
  102. package/dist/code-agents/orchestrator.d.ts +23 -0
  103. package/dist/code-agents/orchestrator.js +403 -0
  104. package/dist/code-agents/parser.d.ts +21 -0
  105. package/dist/code-agents/parser.js +197 -0
  106. package/dist/code-agents/registry.d.ts +27 -0
  107. package/dist/code-agents/registry.js +147 -0
  108. package/dist/code-agents/types.d.ts +66 -0
  109. package/dist/code-agents/types.js +4 -0
  110. package/dist/code-agents/utils.d.ts +36 -0
  111. package/dist/code-agents/utils.js +236 -0
  112. package/dist/config.d.ts +19 -0
  113. package/dist/config.js +123 -0
  114. package/dist/cron.d.ts +49 -0
  115. package/dist/cron.js +400 -0
  116. package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
  117. package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
  118. package/dist/dashboard/favicon.svg +3 -0
  119. package/dist/dashboard/index.html +21 -0
  120. package/dist/dashboard-frontend.d.ts +7 -0
  121. package/dist/dashboard-frontend.js +86 -0
  122. package/dist/dashboard.d.ts +8 -0
  123. package/dist/dashboard.js +4071 -0
  124. package/dist/digests.d.ts +36 -0
  125. package/dist/digests.js +338 -0
  126. package/dist/discord.d.ts +8 -0
  127. package/dist/discord.js +828 -0
  128. package/dist/doctor/checks.d.ts +18 -0
  129. package/dist/doctor/checks.js +368 -0
  130. package/dist/doctor/formatters.d.ts +3 -0
  131. package/dist/doctor/formatters.js +44 -0
  132. package/dist/doctor/index.d.ts +8 -0
  133. package/dist/doctor/index.js +7 -0
  134. package/dist/doctor/runner.d.ts +3 -0
  135. package/dist/doctor/runner.js +109 -0
  136. package/dist/doctor/types.d.ts +20 -0
  137. package/dist/doctor/types.js +1 -0
  138. package/dist/exec-approval.d.ts +101 -0
  139. package/dist/exec-approval.js +432 -0
  140. package/dist/file-lock.d.ts +34 -0
  141. package/dist/file-lock.js +81 -0
  142. package/dist/gateway.d.ts +8 -0
  143. package/dist/gateway.js +114 -0
  144. package/dist/heartbeat.d.ts +4 -0
  145. package/dist/heartbeat.js +101 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.js +75 -0
  148. package/dist/langfuse.d.ts +34 -0
  149. package/dist/langfuse.js +145 -0
  150. package/dist/mcp-context-a8c.d.ts +13 -0
  151. package/dist/mcp-context-a8c.js +34 -0
  152. package/dist/model-selection.d.ts +18 -0
  153. package/dist/model-selection.js +50 -0
  154. package/dist/orchestrator.d.ts +15 -0
  155. package/dist/orchestrator.js +676 -0
  156. package/dist/providers/anthropic.d.ts +7 -0
  157. package/dist/providers/anthropic.js +319 -0
  158. package/dist/providers/codex.d.ts +17 -0
  159. package/dist/providers/codex.js +508 -0
  160. package/dist/providers/content.d.ts +21 -0
  161. package/dist/providers/content.js +55 -0
  162. package/dist/providers/index.d.ts +13 -0
  163. package/dist/providers/index.js +138 -0
  164. package/dist/providers/observability.d.ts +19 -0
  165. package/dist/providers/observability.js +94 -0
  166. package/dist/providers/openai.d.ts +10 -0
  167. package/dist/providers/openai.js +310 -0
  168. package/dist/providers/tool-guard.d.ts +30 -0
  169. package/dist/providers/tool-guard.js +89 -0
  170. package/dist/providers/types.d.ts +34 -0
  171. package/dist/providers/types.js +2 -0
  172. package/dist/providers/utils.d.ts +65 -0
  173. package/dist/providers/utils.js +199 -0
  174. package/dist/security.d.ts +8 -0
  175. package/dist/security.js +113 -0
  176. package/dist/service.d.ts +8 -0
  177. package/dist/service.js +38 -0
  178. package/dist/sessions.d.ts +35 -0
  179. package/dist/sessions.js +142 -0
  180. package/dist/setup.d.ts +36 -0
  181. package/dist/setup.js +821 -0
  182. package/dist/skills-types.d.ts +65 -0
  183. package/dist/skills-types.js +2 -0
  184. package/dist/skills.d.ts +32 -0
  185. package/dist/skills.js +260 -0
  186. package/dist/subagent.d.ts +19 -0
  187. package/dist/subagent.js +376 -0
  188. package/dist/telegram.d.ts +2 -0
  189. package/dist/telegram.js +11 -0
  190. package/dist/tools/bash-tool.d.ts +3 -0
  191. package/dist/tools/bash-tool.js +59 -0
  192. package/dist/tools/browser-tool.d.ts +3 -0
  193. package/dist/tools/browser-tool.js +265 -0
  194. package/dist/tools/definitions.d.ts +432 -0
  195. package/dist/tools/definitions.js +181 -0
  196. package/dist/tools/execute-context.d.ts +26 -0
  197. package/dist/tools/execute-context.js +1 -0
  198. package/dist/tools/file-tools.d.ts +8 -0
  199. package/dist/tools/file-tools.js +67 -0
  200. package/dist/tools/path-utils.d.ts +1 -0
  201. package/dist/tools/path-utils.js +8 -0
  202. package/dist/tools.d.ts +24 -0
  203. package/dist/tools.js +281 -0
  204. package/dist/types.d.ts +259 -0
  205. package/dist/types.js +2 -0
  206. package/dist/usage.d.ts +76 -0
  207. package/dist/usage.js +150 -0
  208. package/dist/voice.d.ts +37 -0
  209. package/dist/voice.js +461 -0
  210. package/package.json +70 -0
  211. package/templates/AGENTS.md +38 -0
  212. package/templates/BOOT.md +23 -0
  213. package/templates/BOOTSTRAP.md +26 -0
  214. package/templates/HEARTBEAT.md +5 -0
  215. package/templates/IDENTITY.md +5 -0
  216. package/templates/MEMORY.md +24 -0
  217. package/templates/SOUL.md +92 -0
  218. package/templates/TOOLS.md +30 -0
  219. package/templates/USER.md +31 -0
@@ -0,0 +1,498 @@
1
+ // Telegram Command Handlers
2
+ import { InlineKeyboard } from 'grammy';
3
+ import { spawnSync } from 'child_process';
4
+ import { getCurrentModel, setCurrentModel, getLastMessage } from '../../gateway.js';
5
+ import { getCronJobs, runCronJob } from '../../cron.js';
6
+ import { runHeartbeatCheck } from '../../heartbeat.js';
7
+ import { cancelTask, getActiveTasks, getRecentTasks } from '../../subagent.js';
8
+ import { getActiveCodeAgents, getRecentCodeAgents } from '../../code-agents/index.js';
9
+ import { listApprovals, approveRequest, denyRequest, getApproval, onApprovalEvent } from '../../exec-approval.js';
10
+ import { loadSkills } from '../../skills.js';
11
+ import { loadRawConfig, saveConfig } from '../../config.js';
12
+ import { readFileSync } from 'fs';
13
+ import { runAgentTurn } from '../../agent.js';
14
+ import { formatAliases, formatModelSelectionError, getModelSelectionUsage, resolveModelSelection } from '../../model-selection.js';
15
+ import { state, LAUNCHD_LABEL } from './types.js';
16
+ import { buildHelpText, getHistory, clearHistory, getRunContext, getTelegramDefaultChatId, getRecentMemoryFiles, startTypingIndicator, sendLongMessage, } from './utils.js';
17
+ // Handler functions for each command
18
+ export async function handleHelp(ctx, cfg) {
19
+ await ctx.reply(buildHelpText(cfg));
20
+ }
21
+ export async function handleStart(ctx, cfg) {
22
+ await ctx.reply(buildHelpText(cfg));
23
+ }
24
+ export async function handleModel(ctx, cfg) {
25
+ const modelAlias = String(ctx.match || '');
26
+ if (!modelAlias) {
27
+ const current = getCurrentModel();
28
+ const aliases = formatAliases(cfg);
29
+ await ctx.reply(`Current: ${current}\nAliases: ${aliases}\n\nUsage: /model <alias|provider/model|model-id>\n${getModelSelectionUsage()}`);
30
+ return;
31
+ }
32
+ const selection = resolveModelSelection(modelAlias, cfg);
33
+ if (!selection.ok || !selection.resolved) {
34
+ const errorMessage = selection.error || 'Invalid model selection';
35
+ await ctx.reply(formatModelSelectionError(errorMessage, cfg));
36
+ return;
37
+ }
38
+ setCurrentModel(selection.resolved);
39
+ if (selection.aliasUsed) {
40
+ await ctx.reply(`Model switched to: ${selection.aliasUsed} (${selection.resolved})`);
41
+ }
42
+ else {
43
+ await ctx.reply(`Model switched to: ${selection.resolved}`);
44
+ }
45
+ }
46
+ export async function handleStatus(ctx, cfg) {
47
+ const model = getCurrentModel();
48
+ const last = getLastMessage();
49
+ const jobs = getCronJobs();
50
+ const activeTasks = getActiveTasks();
51
+ const recentTasks = getRecentTasks(20);
52
+ const jobList = jobs
53
+ .map((j) => ` - ${j.name}: ${j.nextRun?.toLocaleString() || 'unknown'}`)
54
+ .join('\n');
55
+ const pendingCount = activeTasks.filter((t) => t.status === 'pending').length;
56
+ const runningCount = activeTasks.filter((t) => t.status === 'running').length;
57
+ const maxConcurrent = cfg.subagents?.maxConcurrent ?? 5;
58
+ const recentCompleted = recentTasks.filter((t) => t.status === 'completed').length;
59
+ const recentFailed = recentTasks.filter((t) => t.status === 'failed').length;
60
+ const recentCancelled = recentTasks.filter((t) => t.status === 'cancelled').length;
61
+ const activePreview = activeTasks
62
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
63
+ .slice(0, 3)
64
+ .map((task) => {
65
+ const started = task.startedAt || task.createdAt;
66
+ const elapsedSeconds = Math.max(0, Math.round((Date.now() - started.getTime()) / 1000));
67
+ const elapsed = elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.round(elapsedSeconds / 60)}m`;
68
+ const label = task.label ? ` (${task.label})` : '';
69
+ return ` - ${task.id} [${task.type}] ${task.status}${label} • ${elapsed}`;
70
+ })
71
+ .join('\n');
72
+ // Coding agents status (multi-agent)
73
+ const caActive = getActiveCodeAgents();
74
+ const caRecent = getRecentCodeAgents(20);
75
+ const caMap = new Map();
76
+ for (const agent of caActive)
77
+ caMap.set(agent.id, agent);
78
+ for (const agent of caRecent)
79
+ caMap.set(agent.id, agent);
80
+ const caAll = Array.from(caMap.values());
81
+ let caLine = 'Coding Agents: idle';
82
+ if (caAll.length > 0) {
83
+ const runningCount = caAll.filter((t) => t.status === 'running').length;
84
+ const completedCount = caAll.filter((t) => t.status === 'completed').length;
85
+ const failedCount = caAll.filter((t) => t.status === 'failed' || t.status === 'timeout').length;
86
+ const parts = [];
87
+ if (runningCount)
88
+ parts.push(`${runningCount} running`);
89
+ if (completedCount)
90
+ parts.push(`${completedCount} completed`);
91
+ if (failedCount)
92
+ parts.push(`${failedCount} failed`);
93
+ caLine = `Coding Agents: ${parts.join(', ') || 'idle'}`;
94
+ const caPreview = caAll
95
+ .slice(0, 5)
96
+ .map((t) => {
97
+ const elapsed = t.durationSeconds != null
98
+ ? t.durationSeconds < 60 ? `${t.durationSeconds}s` : `${Math.floor(t.durationSeconds / 60)}m ${t.durationSeconds % 60}s`
99
+ : Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1000) + 's';
100
+ const taskPreview = t.task.length > 50 ? t.task.slice(0, 50) + '...' : t.task;
101
+ return ` ${t.id}: ${t.status.toUpperCase()} (${t.agent}, ${elapsed}) — ${taskPreview}`;
102
+ })
103
+ .join('\n');
104
+ if (caPreview)
105
+ caLine += '\n' + caPreview;
106
+ }
107
+ await ctx.reply(`Agent: ${cfg.agents.default}\n` +
108
+ `Model: ${model}\n` +
109
+ `Last message: ${last?.toLocaleString() || 'never'}\n` +
110
+ `Silence until: ${state.silenceUntil?.toLocaleTimeString() || 'not silenced'}\n\n` +
111
+ `${caLine}\n\n` +
112
+ `Subagents: ${activeTasks.length}/${maxConcurrent} active (running: ${runningCount}, pending: ${pendingCount})\n` +
113
+ `Recent (last ${recentTasks.length}): ✅ ${recentCompleted} • ❌ ${recentFailed} • 🚫 ${recentCancelled}\n` +
114
+ `${activePreview ? `Active now:\n${activePreview}\n\n` : '\n'}` +
115
+ `Scheduled jobs:\n${jobList || ' (none)'}`);
116
+ }
117
+ export async function handleCron(ctx, cfg) {
118
+ const args = String(ctx.match || '').split(' ');
119
+ const subcommand = args[0];
120
+ if (subcommand === 'list' || !subcommand) {
121
+ const jobs = getCronJobs();
122
+ if (jobs.length === 0) {
123
+ await ctx.reply('No scheduled jobs.');
124
+ return;
125
+ }
126
+ const list = jobs
127
+ .map((j) => `${j.id}: ${j.name} (next: ${j.nextRun?.toLocaleString() || '?'})`)
128
+ .join('\n');
129
+ await ctx.reply(`Scheduled jobs:\n${list}`);
130
+ return;
131
+ }
132
+ if (subcommand === 'run') {
133
+ const jobId = args[1];
134
+ if (!jobId) {
135
+ await ctx.reply('Usage: /cron run <job-id>');
136
+ return;
137
+ }
138
+ await ctx.replyWithChatAction('typing');
139
+ try {
140
+ await runCronJob(jobId, cfg);
141
+ await ctx.reply(`Triggered: ${jobId}`);
142
+ }
143
+ catch (error) {
144
+ const msg = error instanceof Error ? error.message : 'Unknown error';
145
+ await ctx.reply(`Error: ${msg}`);
146
+ }
147
+ return;
148
+ }
149
+ await ctx.reply('Usage: /cron list | /cron run <id>');
150
+ }
151
+ export async function handleHeartbeat(ctx, cfg) {
152
+ const stopTyping = startTypingIndicator(ctx);
153
+ try {
154
+ const response = await runHeartbeatCheck(cfg);
155
+ await sendLongMessage(ctx, `🫀 ${response}`);
156
+ }
157
+ catch (error) {
158
+ const msg = error instanceof Error ? error.message : 'Unknown error';
159
+ await ctx.reply(`Heartbeat error: ${msg}`);
160
+ }
161
+ finally {
162
+ stopTyping();
163
+ }
164
+ }
165
+ export async function handleRestart(ctx, cfg) {
166
+ const isLaunchd = !!process.env.SKIMPYCLAW_LAUNCHD;
167
+ if (isLaunchd) {
168
+ await ctx.reply('🦞 Restarting via launchd...');
169
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
170
+ const target = uid !== undefined ? `gui/${uid}/${LAUNCHD_LABEL}` : LAUNCHD_LABEL;
171
+ const res = spawnSync('launchctl', ['kickstart', '-k', target], {
172
+ encoding: 'utf8',
173
+ timeout: 3000
174
+ });
175
+ if (res.error || res.status !== 0) {
176
+ await ctx.reply(`Restart failed: ${res.stderr || res.error?.message || 'unknown error'}`);
177
+ }
178
+ }
179
+ else {
180
+ await ctx.reply('🦞 Restarting (dev mode)...');
181
+ setTimeout(() => process.exit(0), 500);
182
+ }
183
+ }
184
+ export async function handleTasks(ctx, cfg) {
185
+ const active = getActiveTasks();
186
+ const recent = getRecentTasks(5);
187
+ if (recent.length === 0) {
188
+ await ctx.reply('No agent tasks yet. Subagents spawn automatically for complex requests.');
189
+ return;
190
+ }
191
+ const formatTask = (t) => {
192
+ const elapsed = ((t.completedAt || new Date()).getTime() - t.createdAt.getTime()) / 1000;
193
+ const elapsedStr = elapsed < 60 ? `${Math.round(elapsed)}s` : `${Math.round(elapsed / 60)}m`;
194
+ const status = {
195
+ pending: '⏳ Pending',
196
+ running: `🔄 Running (${elapsedStr})`,
197
+ completed: `✅ Done (${elapsedStr})`,
198
+ failed: `❌ Failed (${elapsedStr})`,
199
+ cancelled: '🚫 Cancelled'
200
+ };
201
+ const promptPreview = t.prompt.slice(0, 60) + (t.prompt.length > 60 ? '...' : '');
202
+ return `${t.id}: ${status[t.status] || t.status} [${t.type}] ${promptPreview}`;
203
+ };
204
+ const lines = [...active, ...recent].slice(0, 10).map(formatTask).join('\n');
205
+ await ctx.reply(`Agent tasks:\n\n${lines}`);
206
+ }
207
+ export async function handleCancel(ctx, cfg) {
208
+ const id = String(ctx.match || '').trim();
209
+ if (!id) {
210
+ await ctx.reply('Usage: /cancel <task-id>\nExample: /cancel t1');
211
+ return;
212
+ }
213
+ const task = cancelTask(id);
214
+ if (!task) {
215
+ await ctx.reply(`No task found: ${id}`);
216
+ return;
217
+ }
218
+ if (task.status === 'cancelled') {
219
+ await ctx.reply(`Cancelled ${id}.`);
220
+ }
221
+ else {
222
+ await ctx.reply(`Task ${id} is already ${task.status}.`);
223
+ }
224
+ }
225
+ export async function handleSkills(ctx, cfg) {
226
+ const skillConfig = cfg.skills;
227
+ const skills = loadSkills(skillConfig);
228
+ if (skills.length === 0) {
229
+ await ctx.reply('No skills found. Add skills to ~/.skimpyclaw/skills/');
230
+ return;
231
+ }
232
+ const lines = skills.map(s => {
233
+ const emoji = s.frontmatter.emoji || '🔧';
234
+ let status;
235
+ if (!s.eligible) {
236
+ status = `❌ ${s.reason || 'ineligible'}`;
237
+ }
238
+ else if (s.frontmatter.enabled === false) {
239
+ status = '⚠️ disabled';
240
+ }
241
+ else {
242
+ status = '✅ eligible';
243
+ }
244
+ return `${emoji} ${s.name} — ${status}`;
245
+ });
246
+ await sendLongMessage(ctx, `Skills (${skills.length}):\n\n${lines.join('\n')}\n\nUse /skill <name> for details`);
247
+ }
248
+ export async function handleSkill(ctx, cfg) {
249
+ const args = String(ctx.match || '').trim().split(/\s+/);
250
+ const subcommand = args[0]?.toLowerCase();
251
+ if (!subcommand) {
252
+ await ctx.reply('Usage:\n/skill <name> — Show details\n/skill enable <name>\n/skill disable <name>');
253
+ return;
254
+ }
255
+ if (subcommand === 'enable' || subcommand === 'disable') {
256
+ const skillName = args[1];
257
+ if (!skillName) {
258
+ await ctx.reply(`Usage: /skill ${subcommand} <name>`);
259
+ return;
260
+ }
261
+ const enabled = subcommand === 'enable';
262
+ try {
263
+ const raw = loadRawConfig();
264
+ if (!raw.skills)
265
+ raw.skills = {};
266
+ if (!raw.skills.entries)
267
+ raw.skills.entries = {};
268
+ raw.skills.entries[skillName] = enabled;
269
+ saveConfig(raw);
270
+ await ctx.reply(`Skill "${skillName}" ${enabled ? 'enabled' : 'disabled'}.`);
271
+ }
272
+ catch (error) {
273
+ const msg = error instanceof Error ? error.message : 'Unknown error';
274
+ await ctx.reply(`Error: ${msg}`);
275
+ }
276
+ return;
277
+ }
278
+ // Show skill details
279
+ const skillName = subcommand;
280
+ const skillConfig = cfg.skills;
281
+ const skills = loadSkills(skillConfig);
282
+ const skill = skills.find(s => s.name === skillName);
283
+ if (!skill) {
284
+ await ctx.reply(`Skill "${skillName}" not found.\nUse /skills to see available skills.`);
285
+ return;
286
+ }
287
+ const emoji = skill.frontmatter.emoji || '🔧';
288
+ const status = skill.eligible
289
+ ? (skill.frontmatter.enabled !== false ? '✅ Eligible' : '⚠️ Disabled')
290
+ : `❌ ${skill.reason || 'Ineligible'}`;
291
+ const tags = skill.frontmatter.tags?.join(', ') || 'none';
292
+ const contexts = skill.frontmatter.contexts ? JSON.stringify(skill.frontmatter.contexts) : 'all';
293
+ const reqs = skill.frontmatter.requires
294
+ ? Object.entries(skill.frontmatter.requires)
295
+ .filter(([_, v]) => v && v.length > 0)
296
+ .map(([k, v]) => `${k}: ${v.join(', ')}`)
297
+ .join('\n ') || 'none'
298
+ : 'none';
299
+ const detail = `${emoji} ${skill.name}\n\n` +
300
+ `${skill.frontmatter.description}\n\n` +
301
+ `Status: ${status}\n` +
302
+ `Priority: ${skill.frontmatter.priority ?? 100}\n` +
303
+ `Tags: ${tags}\n` +
304
+ `Contexts: ${contexts}\n` +
305
+ `Requires:\n ${reqs}`;
306
+ await sendLongMessage(ctx, detail);
307
+ }
308
+ export async function handleApprovals(ctx, cfg) {
309
+ const pending = listApprovals();
310
+ if (pending.length === 0) {
311
+ await ctx.reply('No pending exec approvals.');
312
+ return;
313
+ }
314
+ for (const approval of pending.slice(0, 10)) {
315
+ const cmdPreview = approval.command.length > 80
316
+ ? approval.command.slice(0, 80) + '...'
317
+ : approval.command;
318
+ const expiresIn = Math.max(0, Math.round((approval.expiresAt.getTime() - Date.now()) / 1000));
319
+ const expiresStr = expiresIn < 60 ? `${expiresIn}s` : `${Math.floor(expiresIn / 60)}m`;
320
+ const keyboard = new InlineKeyboard()
321
+ .text('✅ Approve', `approve:${approval.id}`)
322
+ .text('❌ Deny', `deny:${approval.id}`);
323
+ await ctx.reply(`⛔ Approval #${approval.id}\n` +
324
+ `Tier ${approval.tier}: ${approval.reason}\n` +
325
+ `Command: ${cmdPreview}\n` +
326
+ `${approval.cwd ? `CWD: ${approval.cwd}\n` : ''}` +
327
+ `Expires in: ${expiresStr}`, { reply_markup: keyboard });
328
+ }
329
+ }
330
+ export async function handleApprove(ctx, cfg) {
331
+ const id = String(ctx.match || '').trim();
332
+ if (!id) {
333
+ await ctx.reply('Usage: /approve <id>');
334
+ return;
335
+ }
336
+ const approvedBy = ctx.from?.username || ctx.from?.id?.toString() || 'telegram';
337
+ const success = approveRequest(id, approvedBy);
338
+ if (success) {
339
+ await ctx.reply(`✅ Approved #${id}`);
340
+ }
341
+ else {
342
+ const existing = getApproval(id);
343
+ if (existing) {
344
+ await ctx.reply(`Cannot approve #${id} — status is already "${existing.status}".`);
345
+ }
346
+ else {
347
+ await ctx.reply(`No pending approval found with ID "${id}".`);
348
+ }
349
+ }
350
+ }
351
+ export async function handleDeny(ctx, cfg) {
352
+ const id = String(ctx.match || '').trim();
353
+ if (!id) {
354
+ await ctx.reply('Usage: /deny <id>');
355
+ return;
356
+ }
357
+ const deniedBy = ctx.from?.username || ctx.from?.id?.toString() || 'telegram';
358
+ const success = denyRequest(id, deniedBy);
359
+ if (success) {
360
+ await ctx.reply(`❌ Denied #${id}`);
361
+ }
362
+ else {
363
+ const existing = getApproval(id);
364
+ if (existing) {
365
+ await ctx.reply(`Cannot deny #${id} — status is already "${existing.status}".`);
366
+ }
367
+ else {
368
+ await ctx.reply(`No pending approval found with ID "${id}".`);
369
+ }
370
+ }
371
+ }
372
+ export async function handleNew(ctx, cfg) {
373
+ const chatId = ctx.chat?.id;
374
+ if (chatId)
375
+ await clearHistory(chatId);
376
+ await ctx.reply('Conversation cleared. Starting fresh.');
377
+ }
378
+ export async function handleCompact(ctx, cfg) {
379
+ const chatId = ctx.chat?.id;
380
+ if (!chatId)
381
+ return;
382
+ const history = await getHistory(chatId);
383
+ if (history.length === 0) {
384
+ await ctx.reply('No conversation history to compact.');
385
+ return;
386
+ }
387
+ const stopTyping = startTypingIndicator(ctx);
388
+ try {
389
+ const historyText = history.map((m) => `${m.role}: ${m.content}`).join('\n');
390
+ const summary = await runAgentTurn(cfg.agents.default, `Summarize this conversation in 2-3 sentences so you can remember the context:\n\n${historyText}`, cfg, getCurrentModel(), undefined, undefined, getRunContext(ctx));
391
+ await clearHistory(chatId);
392
+ state.chatHistory.set(chatId, [
393
+ { role: 'user', content: 'Summary of our previous conversation:' },
394
+ { role: 'assistant', content: summary }
395
+ ]);
396
+ state.loadedFromDisk.add(chatId);
397
+ await ctx.reply(`Compacted ${history.length} messages into a summary.`);
398
+ }
399
+ catch (error) {
400
+ const msg = error instanceof Error ? error.message : 'Unknown error';
401
+ await ctx.reply(`Error: ${msg}`);
402
+ }
403
+ finally {
404
+ stopTyping();
405
+ }
406
+ }
407
+ export async function handleSilence(ctx, cfg) {
408
+ const minutes = parseInt(String(ctx.match || '')) || 30;
409
+ state.silenceUntil = new Date(Date.now() + minutes * 60 * 1000);
410
+ await ctx.reply(`Proactive messages silenced until ${state.silenceUntil.toLocaleTimeString()}`);
411
+ }
412
+ export async function handleMemory(ctx, cfg) {
413
+ const arg = String(ctx.match || '').trim();
414
+ const recentFiles = getRecentMemoryFiles(10);
415
+ if (recentFiles.length === 0) {
416
+ await ctx.reply('No memory entries found.');
417
+ return;
418
+ }
419
+ if (arg) {
420
+ const match = recentFiles.find((f) => f.date === arg || f.name === arg || f.name === `${arg}.md`);
421
+ if (!match) {
422
+ await ctx.reply(`No memory entry for "${arg}".\n\nAvailable: ${recentFiles.map((f) => f.date).join(', ')}`);
423
+ return;
424
+ }
425
+ try {
426
+ const content = readFileSync(match.path, 'utf-8');
427
+ const preview = content.length > 3500 ? content.slice(0, 3500) + '\n\n... (truncated)' : content;
428
+ await sendLongMessage(ctx, `📝 Memory: ${match.date}\n\n${preview}`);
429
+ }
430
+ catch (error) {
431
+ const msg = error instanceof Error ? error.message : 'Unknown error';
432
+ await ctx.reply(`Error reading memory: ${msg}`);
433
+ }
434
+ return;
435
+ }
436
+ const formatSize = (bytes) => {
437
+ if (bytes < 1024)
438
+ return `${bytes}B`;
439
+ return `${(bytes / 1024).toFixed(1)}KB`;
440
+ };
441
+ const list = recentFiles.map((f) => ` ${f.date} (${formatSize(f.size)})`).join('\n');
442
+ await ctx.reply(`📝 Recent memory entries:\n\n${list}\n\n` +
443
+ `View one: /memory <date>\nExample: /memory ${recentFiles[0].date}`);
444
+ }
445
+ // Export all handlers
446
+ export const commandHandlers = {
447
+ help: handleHelp,
448
+ start: handleStart,
449
+ model: handleModel,
450
+ status: handleStatus,
451
+ cron: handleCron,
452
+ heartbeat: handleHeartbeat,
453
+ restart: handleRestart,
454
+ tasks: handleTasks,
455
+ cancel: handleCancel,
456
+ skills: handleSkills,
457
+ skill: handleSkill,
458
+ approvals: handleApprovals,
459
+ approve: handleApprove,
460
+ deny: handleDeny,
461
+ new: handleNew,
462
+ compact: handleCompact,
463
+ silence: handleSilence,
464
+ memory: handleMemory,
465
+ };
466
+ // Subscribe to approval events for proactive notifications
467
+ export function subscribeToApprovalEvents(bot, cfg) {
468
+ onApprovalEvent('created', (event) => {
469
+ if (!bot)
470
+ return;
471
+ const { approval } = event;
472
+ const meta = approval.channelMeta;
473
+ if (meta?.channel && meta.channel !== 'telegram')
474
+ return;
475
+ let targetChatId;
476
+ if (meta?.channel === 'telegram' && meta.chatId) {
477
+ targetChatId = typeof meta.chatId === 'number' ? meta.chatId : Number(meta.chatId);
478
+ }
479
+ if (!targetChatId) {
480
+ targetChatId = getTelegramDefaultChatId(cfg) ?? undefined;
481
+ }
482
+ if (!targetChatId || !Number.isFinite(targetChatId))
483
+ return;
484
+ const cmdPreview = approval.command.length > 80 ? approval.command.slice(0, 80) + '...' : approval.command;
485
+ const expiresIn = Math.max(0, Math.round((approval.expiresAt.getTime() - Date.now()) / 1000));
486
+ const expiresStr = expiresIn < 60 ? `${expiresIn}s` : `${Math.floor(expiresIn / 60)}m`;
487
+ const keyboard = new InlineKeyboard()
488
+ .text('✅ Approve', `approve:${approval.id}`)
489
+ .text('❌ Deny', `deny:${approval.id}`);
490
+ bot.api.sendMessage(targetChatId, `⛔ Exec approval needed: #${approval.id}\n` +
491
+ `Tier ${approval.tier}: ${approval.reason}\n` +
492
+ `Command: ${cmdPreview}\n` +
493
+ `${approval.cwd ? `CWD: ${approval.cwd}\n` : ''}` +
494
+ `Expires in: ${expiresStr}`, { reply_markup: keyboard }).catch((err) => {
495
+ console.error('[telegram] Failed to send approval notification:', err);
496
+ });
497
+ });
498
+ }
@@ -0,0 +1,14 @@
1
+ import { Bot } from 'grammy';
2
+ import type { Config } from '../../types.js';
3
+ export { state, BOT_COMMANDS, LAUNCHD_LABEL } from './types.js';
4
+ export { getHistory, addToHistory, clearHistory, getRunContext, getDefaultTelegramToolConfig, buildHelpText, getTelegramDefaultChatId, startTypingIndicator, sendLongMessage, sendLongMessageHtml, escapeHtml, markdownToTelegramHtml, } from './utils.js';
5
+ export { commandHandlers, subscribeToApprovalEvents } from './handlers.js';
6
+ export declare function getBot(): Bot | null;
7
+ export declare function setSilenceUntil(date: Date | null): void;
8
+ export declare function getSilenceUntil(): Date | null;
9
+ export declare function startTelegram(): Promise<void>;
10
+ export declare function stopTelegram(): Promise<void>;
11
+ export declare function isSilenced(): boolean;
12
+ export declare function sendProactiveMessage(chatId: string | number, message: string): Promise<void>;
13
+ export declare function sendProactiveVoice(chatId: string | number, buffer: Buffer, format: string): Promise<void>;
14
+ export declare function initTelegram(cfg: Config): Promise<Bot | null>;