@visorcraft/idlehands 1.3.3 → 1.3.5

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 (70) hide show
  1. package/README.md +28 -1
  2. package/dist/agent/formatting.js +1 -5
  3. package/dist/agent/formatting.js.map +1 -1
  4. package/dist/agent.js +130 -22
  5. package/dist/agent.js.map +1 -1
  6. package/dist/anton/controller.js +20 -1
  7. package/dist/anton/controller.js.map +1 -1
  8. package/dist/anton/reporter.js +2 -20
  9. package/dist/anton/reporter.js.map +1 -1
  10. package/dist/bot/auto-continue.js +24 -0
  11. package/dist/bot/auto-continue.js.map +1 -0
  12. package/dist/bot/commands.js +50 -0
  13. package/dist/bot/commands.js.map +1 -1
  14. package/dist/bot/discord-commands.js +833 -0
  15. package/dist/bot/discord-commands.js.map +1 -0
  16. package/dist/bot/discord-routing.js +1 -8
  17. package/dist/bot/discord-routing.js.map +1 -1
  18. package/dist/bot/discord.js +36 -789
  19. package/dist/bot/discord.js.map +1 -1
  20. package/dist/bot/session-manager.js +52 -0
  21. package/dist/bot/session-manager.js.map +1 -1
  22. package/dist/bot/telegram-commands.js +201 -0
  23. package/dist/bot/telegram-commands.js.map +1 -0
  24. package/dist/bot/telegram.js +32 -310
  25. package/dist/bot/telegram.js.map +1 -1
  26. package/dist/bot/ux/events.js +142 -0
  27. package/dist/bot/ux/events.js.map +1 -0
  28. package/dist/cli/commands/project.js +52 -0
  29. package/dist/cli/commands/project.js.map +1 -1
  30. package/dist/config.js +16 -0
  31. package/dist/config.js.map +1 -1
  32. package/dist/context.js +1 -3
  33. package/dist/context.js.map +1 -1
  34. package/dist/progress/ir.js +0 -3
  35. package/dist/progress/ir.js.map +1 -1
  36. package/dist/progress/tool-summary.js +1 -4
  37. package/dist/progress/tool-summary.js.map +1 -1
  38. package/dist/progress/turn-progress.js +1 -5
  39. package/dist/progress/turn-progress.js.map +1 -1
  40. package/dist/runtime/executor.js +1 -3
  41. package/dist/runtime/executor.js.map +1 -1
  42. package/dist/runtime/health.js +2 -1
  43. package/dist/runtime/health.js.map +1 -1
  44. package/dist/shared/async.js +5 -0
  45. package/dist/shared/async.js.map +1 -0
  46. package/dist/shared/config-utils.js +8 -0
  47. package/dist/shared/config-utils.js.map +1 -0
  48. package/dist/shared/format.js +19 -0
  49. package/dist/shared/format.js.map +1 -0
  50. package/dist/shared/math.js +5 -0
  51. package/dist/shared/math.js.map +1 -0
  52. package/dist/shared/strings.js +8 -0
  53. package/dist/shared/strings.js.map +1 -0
  54. package/dist/tools/patch.js +82 -0
  55. package/dist/tools/patch.js.map +1 -0
  56. package/dist/tools/path-safety.js +89 -0
  57. package/dist/tools/path-safety.js.map +1 -0
  58. package/dist/tools/undo.js +141 -0
  59. package/dist/tools/undo.js.map +1 -0
  60. package/dist/tools.js +11 -289
  61. package/dist/tools.js.map +1 -1
  62. package/dist/tui/controller.js +24 -1
  63. package/dist/tui/controller.js.map +1 -1
  64. package/dist/tui/event-bridge.js +1 -3
  65. package/dist/tui/event-bridge.js.map +1 -1
  66. package/dist/tui/render.js +1 -5
  67. package/dist/tui/render.js.map +1 -1
  68. package/dist/vault.js +1 -5
  69. package/dist/vault.js.map +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,833 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { runAnton } from '../anton/controller.js';
4
+ import { parseTaskFile } from '../anton/parser.js';
5
+ import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip, formatToolLoopEvent, formatCompactionEvent, formatVerificationDetail, } from '../anton/reporter.js';
6
+ import { firstToken } from '../cli/command-utils.js';
7
+ import { PKG_VERSION } from '../utils.js';
8
+ import { detectRepoCandidates, expandHome, isPathAllowed, } from './dir-guard.js';
9
+ import { splitDiscord, } from './discord-routing.js';
10
+ /**
11
+ * Handle text-based commands from Discord messages.
12
+ * Returns true if a command was handled, false if the message should be
13
+ * processed as a regular agent message.
14
+ */
15
+ export async function handleTextCommand(managed, msg, content, ctx) {
16
+ const { sendUserVisible, cancelActive, recreateSession, watchdogStatusText, defaultDir, config, botConfig, approvalMode, maxQueue } = ctx;
17
+ if (content === '/cancel') {
18
+ const res = cancelActive(managed);
19
+ await sendUserVisible(msg, res.message).catch(() => { });
20
+ return true;
21
+ }
22
+ if (content === '/start') {
23
+ const agentLine = managed.agentPersona
24
+ ? `Agent: **${managed.agentPersona.display_name || managed.agentId}**`
25
+ : null;
26
+ const lines = [
27
+ '🔧 Idle Hands — Local-first coding agent',
28
+ '',
29
+ ...(agentLine ? [agentLine] : []),
30
+ `Model: \`${managed.session.model}\``,
31
+ `Endpoint: \`${managed.config.endpoint || '?'}\``,
32
+ `Default dir: \`${managed.config.dir || defaultDir}\``,
33
+ '',
34
+ 'Send me a coding task, or use /help for commands.',
35
+ ];
36
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
37
+ return true;
38
+ }
39
+ if (content === '/help') {
40
+ const lines = [
41
+ 'Commands:',
42
+ '/start — Welcome + config summary',
43
+ '/help — This message',
44
+ '/version — Show version',
45
+ '/new — Start a new session',
46
+ '/cancel — Abort current generation',
47
+ '/status — Session stats',
48
+ '/watchdog [status] — Show watchdog settings/status',
49
+ '/agent — Show current agent',
50
+ '/agents — List all configured agents',
51
+ '/escalate [model] — Use larger model for next message',
52
+ '/deescalate — Return to base model',
53
+ '/dir [path] — Get/set working directory',
54
+ '/pin — Pin current working directory',
55
+ '/unpin — Unpin working directory',
56
+ '/model — Show current model',
57
+ '/approval [mode] — Get/set approval mode',
58
+ '/mode [code|sys] — Get/set mode',
59
+ '/subagents [on|off] — Toggle sub-agents',
60
+ '/compact — Trigger context compaction',
61
+ '/changes — Show files modified this session',
62
+ '/undo — Undo last edit',
63
+ '/vault <query> — Search vault entries',
64
+ '/anton <file> — Start autonomous task runner',
65
+ '/anton status | /anton stop | /anton last',
66
+ ];
67
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
68
+ return true;
69
+ }
70
+ if (content === '/model') {
71
+ await sendUserVisible(msg, `Model: \`${managed.session.model}\`\nHarness: \`${managed.session.harness}\``).catch(() => { });
72
+ return true;
73
+ }
74
+ if (content === '/version') {
75
+ await sendUserVisible(msg, `Idle Hands v${PKG_VERSION}`).catch(() => { });
76
+ return true;
77
+ }
78
+ if (content === '/compact') {
79
+ managed.session.reset();
80
+ await sendUserVisible(msg, '🗜 Session context compacted (reset to system prompt).').catch(() => { });
81
+ return true;
82
+ }
83
+ if (content === '/dir' || content.startsWith('/dir ')) {
84
+ const arg = content.slice('/dir'.length).trim();
85
+ if (!arg) {
86
+ const lines = [
87
+ `Working directory: \`${managed.config.dir || defaultDir}\``,
88
+ `Directory pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
89
+ ];
90
+ if (!managed.dirPinned && managed.repoCandidates.length > 1) {
91
+ lines.push('Action required: run `/dir <repo-root>` before file edits.');
92
+ lines.push(`Detected repos: ${managed.repoCandidates
93
+ .slice(0, 5)
94
+ .map((p) => `\`${p}\``)
95
+ .join(', ')}`);
96
+ }
97
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
98
+ return true;
99
+ }
100
+ const resolvedDir = path.resolve(expandHome(arg));
101
+ if (!isPathAllowed(resolvedDir, managed.allowedDirs)) {
102
+ await sendUserVisible(msg, `❌ Directory not allowed. Allowed roots: ${managed.allowedDirs.map((d) => `\`${d}\``).join(', ')}`).catch(() => { });
103
+ return true;
104
+ }
105
+ const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
106
+ const cfg = {
107
+ ...managed.config,
108
+ dir: resolvedDir,
109
+ allowed_write_roots: managed.allowedDirs,
110
+ dir_pinned: true,
111
+ repo_candidates: repoCandidates,
112
+ };
113
+ await recreateSession(managed, cfg);
114
+ managed.dirPinned = true;
115
+ managed.repoCandidates = repoCandidates;
116
+ await sendUserVisible(msg, `✅ Working directory pinned to \`${resolvedDir}\``).catch(() => { });
117
+ return true;
118
+ }
119
+ if (content === '/pin' || content.startsWith('/pin ')) {
120
+ const arg = content.slice('/pin'.length).trim();
121
+ const currentDir = managed.config.dir || defaultDir;
122
+ if (!arg) {
123
+ const resolvedDir = path.resolve(expandHome(currentDir));
124
+ if (!isPathAllowed(resolvedDir, managed.allowedDirs)) {
125
+ await sendUserVisible(msg, `❌ Directory not allowed. Allowed roots: ${managed.allowedDirs.map((d) => `\`${d}\``).join(', ')}`).catch(() => { });
126
+ return true;
127
+ }
128
+ const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
129
+ const cfg = {
130
+ ...managed.config,
131
+ dir: resolvedDir,
132
+ allowed_write_roots: managed.allowedDirs,
133
+ dir_pinned: true,
134
+ repo_candidates: repoCandidates,
135
+ };
136
+ await recreateSession(managed, cfg);
137
+ managed.dirPinned = true;
138
+ managed.repoCandidates = repoCandidates;
139
+ await sendUserVisible(msg, `✅ Working directory pinned to \`${resolvedDir}\``).catch(() => { });
140
+ return true;
141
+ }
142
+ }
143
+ if (content === '/unpin' || content.startsWith('/unpin ')) {
144
+ if (!managed.dirPinned) {
145
+ await sendUserVisible(msg, 'Directory is not pinned.').catch(() => { });
146
+ return true;
147
+ }
148
+ const currentDir = managed.config.dir || defaultDir;
149
+ const resolvedDir = path.resolve(expandHome(currentDir));
150
+ const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
151
+ const cfg = {
152
+ ...managed.config,
153
+ dir: undefined,
154
+ allowed_write_roots: managed.allowedDirs,
155
+ dir_pinned: false,
156
+ repo_candidates: repoCandidates,
157
+ };
158
+ await recreateSession(managed, cfg);
159
+ managed.dirPinned = false;
160
+ managed.repoCandidates = repoCandidates;
161
+ await sendUserVisible(msg, `✅ Directory unpinned. Working directory remains at \`${resolvedDir}\``).catch(() => { });
162
+ return true;
163
+ }
164
+ if (content === '/approval' || content.startsWith('/approval ')) {
165
+ const arg = content.slice('/approval'.length).trim().toLowerCase();
166
+ const modes = ['plan', 'default', 'auto-edit', 'yolo'];
167
+ if (!arg) {
168
+ await sendUserVisible(msg, `Approval mode: \`${managed.config.approval_mode || approvalMode}\`\nOptions: ${modes.join(', ')}`).catch(() => { });
169
+ return true;
170
+ }
171
+ if (!modes.includes(arg)) {
172
+ await sendUserVisible(msg, `Invalid mode. Options: ${modes.join(', ')}`).catch(() => { });
173
+ return true;
174
+ }
175
+ managed.config.approval_mode = arg;
176
+ managed.config.no_confirm = arg === 'yolo';
177
+ await sendUserVisible(msg, `✅ Approval mode set to \`${arg}\``).catch(() => { });
178
+ return true;
179
+ }
180
+ if (content === '/mode' || content.startsWith('/mode ')) {
181
+ const arg = content.slice('/mode'.length).trim().toLowerCase();
182
+ if (!arg) {
183
+ await sendUserVisible(msg, `Mode: \`${managed.config.mode || 'code'}\``).catch(() => { });
184
+ return true;
185
+ }
186
+ if (arg !== 'code' && arg !== 'sys') {
187
+ await sendUserVisible(msg, 'Invalid mode. Options: code, sys').catch(() => { });
188
+ return true;
189
+ }
190
+ managed.config.mode = arg;
191
+ if (arg === 'sys' && managed.config.approval_mode === 'auto-edit') {
192
+ managed.config.approval_mode = 'default';
193
+ }
194
+ await sendUserVisible(msg, `✅ Mode set to \`${arg}\``).catch(() => { });
195
+ return true;
196
+ }
197
+ if (content === '/subagents' || content.startsWith('/subagents ')) {
198
+ const arg = content.slice('/subagents'.length).trim().toLowerCase();
199
+ const current = managed.config.sub_agents?.enabled !== false;
200
+ if (!arg) {
201
+ await sendUserVisible(msg, `Sub-agents: \`${current ? 'on' : 'off'}\`\nUsage: /subagents on | off`).catch(() => { });
202
+ return true;
203
+ }
204
+ if (arg !== 'on' && arg !== 'off') {
205
+ await sendUserVisible(msg, 'Invalid value. Usage: /subagents on | off').catch(() => { });
206
+ return true;
207
+ }
208
+ const enabled = arg === 'on';
209
+ managed.config.sub_agents = { ...(managed.config.sub_agents ?? {}), enabled };
210
+ await sendUserVisible(msg, `✅ Sub-agents \`${enabled ? 'on' : 'off'}\`${!enabled ? ' — spawn_task disabled for this session' : ''}`).catch(() => { });
211
+ return true;
212
+ }
213
+ if (content === '/changes') {
214
+ const replay = managed.session.replay;
215
+ if (!replay) {
216
+ await sendUserVisible(msg, 'Replay is disabled. No change tracking available.').catch(() => { });
217
+ return true;
218
+ }
219
+ try {
220
+ const checkpoints = await replay.list(50);
221
+ if (!checkpoints.length) {
222
+ await sendUserVisible(msg, 'No file changes this session.').catch(() => { });
223
+ return true;
224
+ }
225
+ const byFile = new Map();
226
+ for (const cp of checkpoints)
227
+ byFile.set(cp.filePath, (byFile.get(cp.filePath) ?? 0) + 1);
228
+ const lines = [`Session changes (${byFile.size} files):`];
229
+ for (const [fp, count] of byFile)
230
+ lines.push(`✎ \`${fp}\` (${count} edit${count > 1 ? 's' : ''})`);
231
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
232
+ }
233
+ catch (e) {
234
+ await sendUserVisible(msg, `Error listing changes: ${e?.message ?? String(e)}`).catch(() => { });
235
+ }
236
+ return true;
237
+ }
238
+ if (content === '/undo') {
239
+ const lastPath = managed.session.lastEditedPath;
240
+ if (!lastPath) {
241
+ await sendUserVisible(msg, 'No recent edits to undo.').catch(() => { });
242
+ return true;
243
+ }
244
+ try {
245
+ const { undo_path } = await import('../tools.js');
246
+ const result = await undo_path({ cwd: managed.config.dir || defaultDir, noConfirm: true, dryRun: false }, { path: lastPath });
247
+ await sendUserVisible(msg, `✅ ${result}`).catch(() => { });
248
+ }
249
+ catch (e) {
250
+ await sendUserVisible(msg, `❌ Undo failed: ${e?.message ?? String(e)}`).catch(() => { });
251
+ }
252
+ return true;
253
+ }
254
+ if (content === '/vault' || content.startsWith('/vault ')) {
255
+ const query = content.slice('/vault'.length).trim();
256
+ if (!query) {
257
+ await sendUserVisible(msg, 'Usage: /vault <search query>').catch(() => { });
258
+ return true;
259
+ }
260
+ const vault = managed.session.vault;
261
+ if (!vault) {
262
+ await sendUserVisible(msg, 'Vault is disabled.').catch(() => { });
263
+ return true;
264
+ }
265
+ try {
266
+ const results = await vault.search(query, 5);
267
+ if (!results.length) {
268
+ await sendUserVisible(msg, `No vault results for "${query}"`).catch(() => { });
269
+ return true;
270
+ }
271
+ const lines = [`Vault results for "${query}":`];
272
+ for (const r of results) {
273
+ const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || '?'}`;
274
+ const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 120);
275
+ lines.push(`• ${title}: ${body}`);
276
+ }
277
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
278
+ }
279
+ catch (e) {
280
+ await sendUserVisible(msg, `Error searching vault: ${e?.message ?? String(e)}`).catch(() => { });
281
+ }
282
+ return true;
283
+ }
284
+ if (content === '/status') {
285
+ const used = managed.session.currentContextTokens;
286
+ const pct = managed.session.contextWindow > 0
287
+ ? Math.min(100, (used / managed.session.contextWindow) * 100).toFixed(1)
288
+ : '?';
289
+ const agentLine = managed.agentPersona
290
+ ? `Agent: ${managed.agentPersona.display_name || managed.agentId}`
291
+ : null;
292
+ await sendUserVisible(msg, [
293
+ ...(agentLine ? [agentLine] : []),
294
+ `Mode: ${managed.config.mode ?? 'code'}`,
295
+ `Approval: ${managed.config.approval_mode}`,
296
+ `Model: ${managed.session.model}`,
297
+ `Harness: ${managed.session.harness}`,
298
+ `Dir: ${managed.config.dir ?? defaultDir}`,
299
+ `Dir pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
300
+ `Context: ~${used}/${managed.session.contextWindow} (${pct}%)`,
301
+ `State: ${managed.state}`,
302
+ `Queue: ${managed.pendingQueue.length}/${maxQueue}`,
303
+ ].join('\n')).catch(() => { });
304
+ return true;
305
+ }
306
+ if (content === '/watchdog' || content === '/watchdog status') {
307
+ await sendUserVisible(msg, watchdogStatusText(managed)).catch(() => { });
308
+ return true;
309
+ }
310
+ if (content.startsWith('/watchdog ')) {
311
+ await sendUserVisible(msg, 'Usage: /watchdog or /watchdog status').catch(() => { });
312
+ return true;
313
+ }
314
+ if (content === '/agent') {
315
+ if (!managed.agentPersona) {
316
+ await sendUserVisible(msg, 'No agent configured. Using global config.').catch(() => { });
317
+ return true;
318
+ }
319
+ const p = managed.agentPersona;
320
+ const lines = [
321
+ `**Agent: ${p.display_name || managed.agentId}** (\`${managed.agentId}\`)`,
322
+ ...(p.model ? [`Model: \`${p.model}\``] : []),
323
+ ...(p.endpoint ? [`Endpoint: \`${p.endpoint}\``] : []),
324
+ ...(p.approval_mode ? [`Approval: \`${p.approval_mode}\``] : []),
325
+ ...(p.default_dir ? [`Default dir: \`${p.default_dir}\``] : []),
326
+ ...(p.allowed_dirs?.length
327
+ ? [`Allowed dirs: ${p.allowed_dirs.map((d) => `\`${d}\``).join(', ')}`]
328
+ : []),
329
+ ];
330
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
331
+ return true;
332
+ }
333
+ if (content === '/agents') {
334
+ const agents = botConfig.agents;
335
+ if (!agents || Object.keys(agents).length === 0) {
336
+ await sendUserVisible(msg, 'No agents configured. Using global config.').catch(() => { });
337
+ return true;
338
+ }
339
+ const lines = ['**Configured Agents:**'];
340
+ for (const [id, p] of Object.entries(agents)) {
341
+ const current = id === managed.agentId ? ' ← current' : '';
342
+ const model = p.model ? ` (${p.model})` : '';
343
+ lines.push(`• **${p.display_name || id}** (\`${id}\`)${model}${current}`);
344
+ }
345
+ const routing = botConfig.routing;
346
+ if (routing) {
347
+ lines.push('', '**Routing:**');
348
+ if (routing.default)
349
+ lines.push(`Default: \`${routing.default}\``);
350
+ if (routing.users && Object.keys(routing.users).length > 0) {
351
+ lines.push(`Users: ${Object.entries(routing.users)
352
+ .map(([u, a]) => `${u}→${a}`)
353
+ .join(', ')}`);
354
+ }
355
+ if (routing.channels && Object.keys(routing.channels).length > 0) {
356
+ lines.push(`Channels: ${Object.entries(routing.channels)
357
+ .map(([c, a]) => `${c}→${a}`)
358
+ .join(', ')}`);
359
+ }
360
+ if (routing.guilds && Object.keys(routing.guilds).length > 0) {
361
+ lines.push(`Guilds: ${Object.entries(routing.guilds)
362
+ .map(([g, a]) => `${g}→${a}`)
363
+ .join(', ')}`);
364
+ }
365
+ }
366
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
367
+ return true;
368
+ }
369
+ if (content === '/escalate' || content.startsWith('/escalate ')) {
370
+ const escalation = managed.agentPersona?.escalation;
371
+ if (!escalation || !escalation.models?.length) {
372
+ await sendUserVisible(msg, '❌ No escalation models configured for this agent.').catch(() => { });
373
+ return true;
374
+ }
375
+ const arg = content.slice('/escalate'.length).trim();
376
+ if (!arg) {
377
+ const currentModel = managed.config.model || config.model || 'default';
378
+ const lines = [
379
+ `**Current model:** \`${currentModel}\``,
380
+ `**Escalation models:** ${escalation.models.map((m) => `\`${m}\``).join(', ')}`,
381
+ '',
382
+ 'Usage: `/escalate <model>` or `/escalate next`',
383
+ 'Then send your message - it will use the escalated model.',
384
+ ];
385
+ if (managed.pendingEscalation) {
386
+ lines.push('', `⚡ **Pending escalation:** \`${managed.pendingEscalation}\` (next message will use this)`);
387
+ }
388
+ await sendUserVisible(msg, lines.join('\n')).catch(() => { });
389
+ return true;
390
+ }
391
+ let targetModel;
392
+ if (arg.toLowerCase() === 'next') {
393
+ const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
394
+ targetModel = escalation.models[nextIndex];
395
+ }
396
+ else {
397
+ if (!escalation.models.includes(arg)) {
398
+ await sendUserVisible(msg, `❌ Model \`${arg}\` not in escalation chain. Available: ${escalation.models.map((m) => `\`${m}\``).join(', ')}`).catch(() => { });
399
+ return true;
400
+ }
401
+ targetModel = arg;
402
+ }
403
+ managed.pendingEscalation = targetModel;
404
+ await sendUserVisible(msg, `⚡ Next message will use \`${targetModel}\`. Send your request now.`).catch(() => { });
405
+ return true;
406
+ }
407
+ if (content === '/deescalate') {
408
+ if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
409
+ await sendUserVisible(msg, 'Already using base model.').catch(() => { });
410
+ return true;
411
+ }
412
+ const baseModel = managed.agentPersona?.model || config.model || 'default';
413
+ managed.pendingEscalation = null;
414
+ managed.currentModelIndex = 0;
415
+ const cfg = {
416
+ ...managed.config,
417
+ model: baseModel,
418
+ };
419
+ await recreateSession(managed, cfg);
420
+ await sendUserVisible(msg, `✅ Returned to base model: \`${baseModel}\``).catch(() => { });
421
+ return true;
422
+ }
423
+ if (content === '/git_status') {
424
+ const cwd = managed.config.dir || defaultDir;
425
+ if (!cwd) {
426
+ await sendUserVisible(msg, 'No working directory set. Use `/dir` to set one.').catch(() => { });
427
+ return true;
428
+ }
429
+ try {
430
+ const { spawnSync } = await import('node:child_process');
431
+ const statusResult = spawnSync('git', ['status', '-s'], {
432
+ cwd,
433
+ encoding: 'utf8',
434
+ timeout: 5000,
435
+ });
436
+ if (statusResult.status !== 0) {
437
+ const err = String(statusResult.stderr || statusResult.error || 'Unknown error');
438
+ if (err.includes('not a git repository') || err.includes('not in a git')) {
439
+ await sendUserVisible(msg, '❌ Not a git repository.').catch(() => { });
440
+ }
441
+ else {
442
+ await sendUserVisible(msg, `❌ git status failed: ${err.slice(0, 200)}`).catch(() => { });
443
+ }
444
+ return true;
445
+ }
446
+ const statusOut = String(statusResult.stdout || '').trim();
447
+ const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
448
+ cwd,
449
+ encoding: 'utf8',
450
+ timeout: 2000,
451
+ });
452
+ const branch = branchResult.status === 0 ? String(branchResult.stdout || '').trim() : 'unknown';
453
+ if (!statusOut) {
454
+ await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n✅ Working tree clean`).catch(() => { });
455
+ return true;
456
+ }
457
+ const lines = statusOut.split('\n').slice(0, 30);
458
+ const truncated = statusOut.split('\n').length > 30;
459
+ const formatted = lines
460
+ .map((line) => `\`${line.slice(0, 2)}\` ${line.slice(3)}`)
461
+ .join('\n');
462
+ await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n\`\`\`\n${formatted}${truncated ? '\n...' : ''}\`\`\``).catch(() => { });
463
+ }
464
+ catch (e) {
465
+ await sendUserVisible(msg, `❌ git status failed: ${e?.message ?? String(e)}`).catch(() => { });
466
+ }
467
+ return true;
468
+ }
469
+ if (content === '/hosts') {
470
+ try {
471
+ const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
472
+ const config = await loadRuntimes();
473
+ const redacted = redactConfig(config);
474
+ if (!redacted.hosts.length) {
475
+ await sendUserVisible(msg, 'No hosts configured. Use `idlehands hosts add` in CLI.').catch(() => { });
476
+ return true;
477
+ }
478
+ const lines = redacted.hosts.map((h) => `${h.enabled ? '🟢' : '🔴'} ${h.display_name} (\`${h.id}\`)\n Transport: ${h.transport}`);
479
+ const chunks = splitDiscord(lines.join('\n\n'));
480
+ for (const [i, chunk] of chunks.entries()) {
481
+ if (i === 0)
482
+ await sendUserVisible(msg, chunk).catch(() => { });
483
+ else
484
+ await msg.channel.send(chunk).catch(() => { });
485
+ }
486
+ }
487
+ catch (e) {
488
+ await sendUserVisible(msg, `❌ Failed to load hosts: ${e?.message ?? String(e)}`).catch(() => { });
489
+ }
490
+ return true;
491
+ }
492
+ if (content === '/backends') {
493
+ try {
494
+ const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
495
+ const config = await loadRuntimes();
496
+ const redacted = redactConfig(config);
497
+ if (!redacted.backends.length) {
498
+ await sendUserVisible(msg, 'No backends configured. Use `idlehands backends add` in CLI.').catch(() => { });
499
+ return true;
500
+ }
501
+ const lines = redacted.backends.map((b) => `${b.enabled ? '🟢' : '🔴'} ${b.display_name} (\`${b.id}\`)\n Type: ${b.type}`);
502
+ const chunks = splitDiscord(lines.join('\n\n'));
503
+ for (const [i, chunk] of chunks.entries()) {
504
+ if (i === 0)
505
+ await sendUserVisible(msg, chunk).catch(() => { });
506
+ else
507
+ await msg.channel.send(chunk).catch(() => { });
508
+ }
509
+ }
510
+ catch (e) {
511
+ await sendUserVisible(msg, `❌ Failed to load backends: ${e?.message ?? String(e)}`).catch(() => { });
512
+ }
513
+ return true;
514
+ }
515
+ if (content === '/models' || content === '/rtmodels') {
516
+ try {
517
+ const { loadRuntimes } = await import('../runtime/store.js');
518
+ const config = await loadRuntimes();
519
+ if (!config.models.length) {
520
+ await sendUserVisible(msg, 'No runtime models configured.').catch(() => { });
521
+ return true;
522
+ }
523
+ const enabledModels = config.models.filter((m) => m.enabled);
524
+ if (!enabledModels.length) {
525
+ await sendUserVisible(msg, 'No enabled runtime models. Use `idlehands models enable <id>` in CLI.').catch(() => { });
526
+ return true;
527
+ }
528
+ const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import('discord.js');
529
+ const rows = [];
530
+ let currentRow = new ActionRowBuilder();
531
+ for (const m of enabledModels) {
532
+ const btn = new ButtonBuilder()
533
+ .setCustomId(`model_switch:${m.id}`)
534
+ .setLabel(m.display_name.slice(0, 80))
535
+ .setStyle(ButtonStyle.Primary);
536
+ currentRow.addComponents(btn);
537
+ if (currentRow.components.length >= 5) {
538
+ rows.push(currentRow);
539
+ currentRow = new ActionRowBuilder();
540
+ }
541
+ }
542
+ if (currentRow.components.length > 0) {
543
+ rows.push(currentRow);
544
+ }
545
+ await msg.channel.send({
546
+ content: '📋 **Select a model to switch to:**',
547
+ components: rows.slice(0, 5),
548
+ }).catch(() => { });
549
+ }
550
+ catch (e) {
551
+ await sendUserVisible(msg, `❌ Failed to load runtime models: ${e?.message ?? String(e)}`).catch(() => { });
552
+ }
553
+ return true;
554
+ }
555
+ if (content === '/rtstatus') {
556
+ try {
557
+ const { loadActiveRuntime } = await import('../runtime/executor.js');
558
+ const active = await loadActiveRuntime();
559
+ if (!active) {
560
+ await sendUserVisible(msg, 'No active runtime.').catch(() => { });
561
+ return true;
562
+ }
563
+ const lines = [
564
+ 'Active Runtime',
565
+ `Model: \`${active.modelId}\``,
566
+ `Backend: \`${active.backendId ?? 'none'}\``,
567
+ `Hosts: ${active.hostIds.map((id) => `\`${id}\``).join(', ') || 'none'}`,
568
+ `Healthy: ${active.healthy ? '✅ yes' : '❌ no'}`,
569
+ `Endpoint: \`${active.endpoint ?? 'unknown'}\``,
570
+ `Started: \`${active.startedAt}\``,
571
+ ];
572
+ const chunks = splitDiscord(lines.join('\n'));
573
+ for (const [i, chunk] of chunks.entries()) {
574
+ if (i === 0)
575
+ await sendUserVisible(msg, chunk).catch(() => { });
576
+ else
577
+ await msg.channel.send(chunk).catch(() => { });
578
+ }
579
+ }
580
+ catch (e) {
581
+ await sendUserVisible(msg, `❌ Failed to read runtime status: ${e?.message ?? String(e)}`).catch(() => { });
582
+ }
583
+ return true;
584
+ }
585
+ if (content === '/switch' || content.startsWith('/switch ')) {
586
+ try {
587
+ const modelId = content.slice('/switch'.length).trim();
588
+ if (!modelId) {
589
+ await sendUserVisible(msg, 'Usage: /switch <model-id>').catch(() => { });
590
+ return true;
591
+ }
592
+ const { plan } = await import('../runtime/planner.js');
593
+ const { execute, loadActiveRuntime } = await import('../runtime/executor.js');
594
+ const { loadRuntimes } = await import('../runtime/store.js');
595
+ const rtConfig = await loadRuntimes();
596
+ const active = await loadActiveRuntime();
597
+ const result = plan({ modelId, mode: 'live' }, rtConfig, active);
598
+ if (!result.ok) {
599
+ await sendUserVisible(msg, `❌ Plan failed: ${result.reason}`).catch(() => { });
600
+ return true;
601
+ }
602
+ if (result.reuse) {
603
+ await sendUserVisible(msg, '✅ Runtime already active and healthy.').catch(() => { });
604
+ return true;
605
+ }
606
+ const statusMsg = await sendUserVisible(msg, `⏳ Switching to \`${result.model.display_name}\`...`).catch(() => null);
607
+ const execResult = await execute(result, {
608
+ onStep: async (step, status) => {
609
+ if (status === 'done' && statusMsg) {
610
+ await statusMsg.edit(`⏳ ${step.description}... ✓`).catch(() => { });
611
+ }
612
+ },
613
+ confirm: async (prompt) => {
614
+ await sendUserVisible(msg, `⚠️ ${prompt}\nAuto-approving for bot context.`).catch(() => { });
615
+ return true;
616
+ },
617
+ });
618
+ if (execResult.ok) {
619
+ if (statusMsg) {
620
+ await statusMsg.edit(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
621
+ }
622
+ else {
623
+ await sendUserVisible(msg, `✅ Switched to \`${result.model.display_name}\``).catch(() => { });
624
+ }
625
+ }
626
+ else {
627
+ const err = `❌ Switch failed: ${execResult.error || 'unknown error'}`;
628
+ if (statusMsg) {
629
+ await statusMsg.edit(err).catch(() => { });
630
+ }
631
+ else {
632
+ await sendUserVisible(msg, err).catch(() => { });
633
+ }
634
+ }
635
+ }
636
+ catch (e) {
637
+ await sendUserVisible(msg, `❌ Switch failed: ${e?.message ?? String(e)}`).catch(() => { });
638
+ }
639
+ return true;
640
+ }
641
+ if (content === '/anton' || content.startsWith('/anton ')) {
642
+ await handleDiscordAnton(managed, msg, content, ctx);
643
+ return true;
644
+ }
645
+ return false;
646
+ }
647
+ const DISCORD_RATE_LIMIT_MS = 15_000;
648
+ export async function handleDiscordAnton(managed, msg, content, ctx) {
649
+ const { sendUserVisible } = ctx;
650
+ const args = content.replace(/^\/anton\s*/, '').trim();
651
+ const sub = firstToken(args);
652
+ if (!sub || sub === 'status') {
653
+ if (!managed.antonActive) {
654
+ await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
655
+ }
656
+ else if (managed.antonAbortSignal?.aborted) {
657
+ await sendUserVisible(msg, '🛑 Anton is stopping. Please wait for the current attempt to unwind.').catch(() => { });
658
+ }
659
+ else if (managed.antonProgress) {
660
+ const line1 = formatProgressBar(managed.antonProgress);
661
+ if (managed.antonProgress.currentTask) {
662
+ await sendUserVisible(msg, `${line1}\n\n**Working on:** *${managed.antonProgress.currentTask}* (Attempt ${managed.antonProgress.currentAttempt})`).catch(() => { });
663
+ }
664
+ else {
665
+ await sendUserVisible(msg, line1).catch(() => { });
666
+ }
667
+ }
668
+ else {
669
+ await sendUserVisible(msg, '🤖 Anton is running (no progress data yet).').catch(() => { });
670
+ }
671
+ return;
672
+ }
673
+ if (sub === 'stop') {
674
+ if (!managed.antonActive || !managed.antonAbortSignal) {
675
+ await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
676
+ return;
677
+ }
678
+ managed.lastActivity = Date.now();
679
+ managed.antonAbortSignal.aborted = true;
680
+ await sendUserVisible(msg, '🛑 Anton stop requested.').catch(() => { });
681
+ return;
682
+ }
683
+ if (sub === 'last') {
684
+ if (!managed.antonLastResult) {
685
+ await sendUserVisible(msg, 'No previous Anton run.').catch(() => { });
686
+ return;
687
+ }
688
+ await sendUserVisible(msg, formatRunSummary(managed.antonLastResult)).catch(() => { });
689
+ return;
690
+ }
691
+ const filePart = sub === 'run' ? args.replace(/^\S+\s*/, '').trim() : args;
692
+ if (!filePart) {
693
+ await sendUserVisible(msg, '/anton <file> — start | /anton status | /anton stop | /anton last').catch(() => { });
694
+ return;
695
+ }
696
+ if (managed.antonActive) {
697
+ const staleMs = Date.now() - managed.lastActivity;
698
+ if (staleMs > 120_000) {
699
+ managed.antonActive = false;
700
+ managed.antonAbortSignal = null;
701
+ managed.antonProgress = null;
702
+ await sendUserVisible(msg, '♻️ Recovered stale Anton run state. Starting a fresh run...').catch(() => { });
703
+ }
704
+ else {
705
+ const runningMsg = managed.antonAbortSignal?.aborted
706
+ ? '🛑 Anton is still stopping. Please wait a moment, then try again.'
707
+ : '⚠️ Anton is already running. Use /anton stop first.';
708
+ await sendUserVisible(msg, runningMsg).catch(() => { });
709
+ return;
710
+ }
711
+ }
712
+ const cwd = managed.config.dir || process.cwd();
713
+ const filePath = path.resolve(cwd, filePart);
714
+ try {
715
+ await fs.stat(filePath);
716
+ }
717
+ catch {
718
+ await sendUserVisible(msg, `File not found: ${filePath}`).catch(() => { });
719
+ return;
720
+ }
721
+ const defaults = managed.config.anton || {};
722
+ const runConfig = {
723
+ taskFile: filePath,
724
+ projectDir: defaults.project_dir || cwd,
725
+ maxRetriesPerTask: defaults.max_retries ?? 3,
726
+ maxIterations: defaults.max_iterations ?? 200,
727
+ taskMaxIterations: defaults.task_max_iterations ?? 50,
728
+ taskTimeoutSec: defaults.task_timeout_sec ?? 600,
729
+ totalTimeoutSec: defaults.total_timeout_sec ?? 7200,
730
+ maxTotalTokens: defaults.max_total_tokens ?? Infinity,
731
+ maxPromptTokensPerAttempt: defaults.max_prompt_tokens_per_attempt ?? 128_000,
732
+ autoCommit: defaults.auto_commit ?? true,
733
+ branch: false,
734
+ allowDirty: false,
735
+ aggressiveCleanOnFail: false,
736
+ verifyAi: defaults.verify_ai ?? true,
737
+ verifyModel: undefined,
738
+ decompose: defaults.decompose ?? true,
739
+ maxDecomposeDepth: defaults.max_decompose_depth ?? 2,
740
+ maxTotalTasks: defaults.max_total_tasks ?? 500,
741
+ buildCommand: defaults.build_command ?? undefined,
742
+ testCommand: defaults.test_command ?? undefined,
743
+ lintCommand: defaults.lint_command ?? undefined,
744
+ skipOnFail: defaults.skip_on_fail ?? false,
745
+ skipOnBlocked: defaults.skip_on_blocked ?? true,
746
+ rollbackOnFail: defaults.rollback_on_fail ?? false,
747
+ maxIdenticalFailures: defaults.max_identical_failures ?? 5,
748
+ approvalMode: (defaults.approval_mode ?? 'yolo'),
749
+ verbose: false,
750
+ dryRun: false,
751
+ };
752
+ const abortSignal = { aborted: false };
753
+ managed.antonActive = true;
754
+ managed.antonAbortSignal = abortSignal;
755
+ managed.antonProgress = null;
756
+ let lastProgressAt = 0;
757
+ const channel = msg.channel;
758
+ const progress = {
759
+ onTaskStart(task, attempt, prog) {
760
+ managed.antonProgress = prog;
761
+ managed.lastActivity = Date.now();
762
+ const now = Date.now();
763
+ if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
764
+ lastProgressAt = now;
765
+ channel.send(formatTaskStart(task, attempt, prog)).catch(() => { });
766
+ }
767
+ },
768
+ onTaskEnd(task, result, prog) {
769
+ managed.antonProgress = prog;
770
+ managed.lastActivity = Date.now();
771
+ const now = Date.now();
772
+ if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
773
+ lastProgressAt = now;
774
+ channel.send(formatTaskEnd(task, result, prog)).catch(() => { });
775
+ }
776
+ },
777
+ onTaskSkip(task, reason) {
778
+ managed.lastActivity = Date.now();
779
+ channel.send(formatTaskSkip(task, reason)).catch(() => { });
780
+ },
781
+ onRunComplete(result) {
782
+ managed.lastActivity = Date.now();
783
+ managed.antonLastResult = result;
784
+ managed.antonActive = false;
785
+ managed.antonAbortSignal = null;
786
+ managed.antonProgress = null;
787
+ channel.send(formatRunSummary(result)).catch(() => { });
788
+ },
789
+ onHeartbeat() {
790
+ managed.lastActivity = Date.now();
791
+ },
792
+ onToolLoop(taskText, event) {
793
+ managed.lastActivity = Date.now();
794
+ if (defaults.progress_events !== false) {
795
+ channel.send(formatToolLoopEvent(taskText, event)).catch(() => { });
796
+ }
797
+ },
798
+ onCompaction(taskText, event) {
799
+ managed.lastActivity = Date.now();
800
+ if (defaults.progress_events !== false && event.droppedMessages >= 5) {
801
+ channel.send(formatCompactionEvent(taskText, event)).catch(() => { });
802
+ }
803
+ },
804
+ onVerification(taskText, verification) {
805
+ managed.lastActivity = Date.now();
806
+ if (defaults.progress_events !== false && !verification.passed) {
807
+ channel.send(formatVerificationDetail(taskText, verification)).catch(() => { });
808
+ }
809
+ },
810
+ };
811
+ let pendingCount = 0;
812
+ try {
813
+ const tf = await parseTaskFile(filePath);
814
+ pendingCount = tf.pending.length;
815
+ }
816
+ catch { }
817
+ await sendUserVisible(msg, `🤖 Anton started on ${filePart} (${pendingCount} tasks pending)`).catch(() => { });
818
+ runAnton({
819
+ config: runConfig,
820
+ idlehandsConfig: managed.config,
821
+ progress,
822
+ abortSignal,
823
+ vault: managed.session.vault,
824
+ lens: managed.session.lens,
825
+ }).catch((err) => {
826
+ managed.lastActivity = Date.now();
827
+ managed.antonActive = false;
828
+ managed.antonAbortSignal = null;
829
+ managed.antonProgress = null;
830
+ channel.send(`Anton error: ${err.message}`).catch(() => { });
831
+ });
832
+ }
833
+ //# sourceMappingURL=discord-commands.js.map