@visorcraft/idlehands 1.3.5 → 1.3.6

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.
@@ -1,12 +1,22 @@
1
- import fs from 'node:fs/promises';
1
+ /**
2
+ * Discord bot command handlers.
3
+ *
4
+ * Business logic lives in command-logic.ts; this file is a thin dispatcher
5
+ * that maps Discord message → shared logic → Markdown reply.
6
+ */
2
7
  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
8
  import { PKG_VERSION } from '../utils.js';
9
+ import { formatMarkdown } from './command-format.js';
10
+ import { startCommand, helpCommand, modelCommand, compactCommand, statusCommand, dirShowCommand, approvalShowCommand, approvalSetCommand, modeShowCommand, modeSetCommand, subagentsShowCommand, subagentsSetCommand, changesCommand, undoCommand, vaultCommand, agentCommand, agentsCommand, escalateShowCommand, escalateSetCommand, deescalateCommand, gitStatusCommand, antonCommand, } from './command-logic.js';
8
11
  import { detectRepoCandidates, expandHome, isPathAllowed, } from './dir-guard.js';
9
12
  import { splitDiscord, } from './discord-routing.js';
13
+ /** Send a CmdResult formatted as Discord Markdown. */
14
+ async function sendResult(msg, sendUserVisible, result) {
15
+ const text = formatMarkdown(result);
16
+ if (!text)
17
+ return;
18
+ await sendUserVisible(msg, text).catch(() => { });
19
+ }
10
20
  /**
11
21
  * Handle text-based commands from Discord messages.
12
22
  * Returns true if a command was handled, false if the message should be
@@ -14,61 +24,30 @@ import { splitDiscord, } from './discord-routing.js';
14
24
  */
15
25
  export async function handleTextCommand(managed, msg, content, ctx) {
16
26
  const { sendUserVisible, cancelActive, recreateSession, watchdogStatusText, defaultDir, config, botConfig, approvalMode, maxQueue } = ctx;
27
+ const m = managed;
28
+ const send = (r) => sendResult(msg, sendUserVisible, r);
17
29
  if (content === '/cancel') {
18
30
  const res = cancelActive(managed);
19
31
  await sendUserVisible(msg, res.message).catch(() => { });
20
32
  return true;
21
33
  }
22
34
  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(() => { });
35
+ await send(startCommand({
36
+ model: managed.session.model,
37
+ endpoint: managed.config.endpoint || '?',
38
+ defaultDir: managed.config.dir || defaultDir,
39
+ agentName: managed.agentPersona
40
+ ? (managed.agentPersona.display_name || managed.agentId)
41
+ : undefined,
42
+ }));
37
43
  return true;
38
44
  }
39
45
  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(() => { });
46
+ await send(helpCommand('discord'));
68
47
  return true;
69
48
  }
70
49
  if (content === '/model') {
71
- await sendUserVisible(msg, `Model: \`${managed.session.model}\`\nHarness: \`${managed.session.harness}\``).catch(() => { });
50
+ await send(modelCommand(m));
72
51
  return true;
73
52
  }
74
53
  if (content === '/version') {
@@ -76,25 +55,13 @@ export async function handleTextCommand(managed, msg, content, ctx) {
76
55
  return true;
77
56
  }
78
57
  if (content === '/compact') {
79
- managed.session.reset();
80
- await sendUserVisible(msg, '🗜 Session context compacted (reset to system prompt).').catch(() => { });
58
+ await send(compactCommand(m));
81
59
  return true;
82
60
  }
83
61
  if (content === '/dir' || content.startsWith('/dir ')) {
84
62
  const arg = content.slice('/dir'.length).trim();
85
63
  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(() => { });
64
+ await send(dirShowCommand(m));
98
65
  return true;
99
66
  }
100
67
  const resolvedDir = path.resolve(expandHome(arg));
@@ -163,144 +130,49 @@ export async function handleTextCommand(managed, msg, content, ctx) {
163
130
  }
164
131
  if (content === '/approval' || content.startsWith('/approval ')) {
165
132
  const arg = content.slice('/approval'.length).trim().toLowerCase();
166
- const modes = ['plan', 'default', 'auto-edit', 'yolo'];
167
133
  if (!arg) {
168
- await sendUserVisible(msg, `Approval mode: \`${managed.config.approval_mode || approvalMode}\`\nOptions: ${modes.join(', ')}`).catch(() => { });
134
+ await send(approvalShowCommand(m, approvalMode));
169
135
  return true;
170
136
  }
171
- if (!modes.includes(arg)) {
172
- await sendUserVisible(msg, `Invalid mode. Options: ${modes.join(', ')}`).catch(() => { });
173
- return true;
137
+ const result = approvalSetCommand(m, arg);
138
+ if (result) {
139
+ await send(result);
174
140
  }
175
- managed.config.approval_mode = arg;
176
- managed.config.no_confirm = arg === 'yolo';
177
- await sendUserVisible(msg, `✅ Approval mode set to \`${arg}\``).catch(() => { });
178
141
  return true;
179
142
  }
180
143
  if (content === '/mode' || content.startsWith('/mode ')) {
181
144
  const arg = content.slice('/mode'.length).trim().toLowerCase();
182
145
  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(() => { });
146
+ await send(modeShowCommand(m));
188
147
  return true;
189
148
  }
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(() => { });
149
+ await send(modeSetCommand(m, arg));
195
150
  return true;
196
151
  }
197
152
  if (content === '/subagents' || content.startsWith('/subagents ')) {
198
153
  const arg = content.slice('/subagents'.length).trim().toLowerCase();
199
- const current = managed.config.sub_agents?.enabled !== false;
200
154
  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(() => { });
155
+ await send(subagentsShowCommand(m));
206
156
  return true;
207
157
  }
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(() => { });
158
+ await send(subagentsSetCommand(m, arg));
211
159
  return true;
212
160
  }
213
161
  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
- }
162
+ await send(await changesCommand(m));
236
163
  return true;
237
164
  }
238
165
  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
- }
166
+ await send(await undoCommand(m));
252
167
  return true;
253
168
  }
254
169
  if (content === '/vault' || content.startsWith('/vault ')) {
255
170
  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
- }
171
+ await send(await vaultCommand(m, query));
282
172
  return true;
283
173
  }
284
174
  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(() => { });
175
+ await send(statusCommand(m, { maxQueue }));
304
176
  return true;
305
177
  }
306
178
  if (content === '/watchdog' || content === '/watchdog status') {
@@ -312,58 +184,14 @@ export async function handleTextCommand(managed, msg, content, ctx) {
312
184
  return true;
313
185
  }
314
186
  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(() => { });
187
+ await send(agentCommand(m));
331
188
  return true;
332
189
  }
333
190
  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(() => { });
191
+ await send(agentsCommand(m, {
192
+ agents: botConfig.agents,
193
+ routing: botConfig.routing,
194
+ }));
367
195
  return true;
368
196
  }
369
197
  if (content === '/escalate' || content.startsWith('/escalate ')) {
@@ -375,43 +203,19 @@ export async function handleTextCommand(managed, msg, content, ctx) {
375
203
  const arg = content.slice('/escalate'.length).trim();
376
204
  if (!arg) {
377
205
  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(() => { });
206
+ await send(escalateShowCommand(m, currentModel));
389
207
  return true;
390
208
  }
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(() => { });
209
+ await send(escalateSetCommand(m, arg));
405
210
  return true;
406
211
  }
407
212
  if (content === '/deescalate') {
408
- if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
409
- await sendUserVisible(msg, 'Already using base model.').catch(() => { });
213
+ const baseModel = managed.agentPersona?.model || config.model || 'default';
214
+ const result = deescalateCommand(m, baseModel);
215
+ if (result !== 'recreate') {
216
+ await send(result);
410
217
  return true;
411
218
  }
412
- const baseModel = managed.agentPersona?.model || config.model || 'default';
413
- managed.pendingEscalation = null;
414
- managed.currentModelIndex = 0;
415
219
  const cfg = {
416
220
  ...managed.config,
417
221
  model: baseModel,
@@ -427,50 +231,19 @@ export async function handleTextCommand(managed, msg, content, ctx) {
427
231
  return true;
428
232
  }
429
233
  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(() => { });
234
+ await send(await gitStatusCommand(cwd));
463
235
  }
464
236
  catch (e) {
465
237
  await sendUserVisible(msg, `❌ git status failed: ${e?.message ?? String(e)}`).catch(() => { });
466
238
  }
467
239
  return true;
468
240
  }
241
+ // ── Discord-only commands (runtime/infra) ──────────────────────────
469
242
  if (content === '/hosts') {
470
243
  try {
471
244
  const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
472
- const config = await loadRuntimes();
473
- const redacted = redactConfig(config);
245
+ const rtConfig = await loadRuntimes();
246
+ const redacted = redactConfig(rtConfig);
474
247
  if (!redacted.hosts.length) {
475
248
  await sendUserVisible(msg, 'No hosts configured. Use `idlehands hosts add` in CLI.').catch(() => { });
476
249
  return true;
@@ -492,8 +265,8 @@ export async function handleTextCommand(managed, msg, content, ctx) {
492
265
  if (content === '/backends') {
493
266
  try {
494
267
  const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
495
- const config = await loadRuntimes();
496
- const redacted = redactConfig(config);
268
+ const rtConfig = await loadRuntimes();
269
+ const redacted = redactConfig(rtConfig);
497
270
  if (!redacted.backends.length) {
498
271
  await sendUserVisible(msg, 'No backends configured. Use `idlehands backends add` in CLI.').catch(() => { });
499
272
  return true;
@@ -515,12 +288,12 @@ export async function handleTextCommand(managed, msg, content, ctx) {
515
288
  if (content === '/models' || content === '/rtmodels') {
516
289
  try {
517
290
  const { loadRuntimes } = await import('../runtime/store.js');
518
- const config = await loadRuntimes();
519
- if (!config.models.length) {
291
+ const rtConfig = await loadRuntimes();
292
+ if (!rtConfig.models.length) {
520
293
  await sendUserVisible(msg, 'No runtime models configured.').catch(() => { });
521
294
  return true;
522
295
  }
523
- const enabledModels = config.models.filter((m) => m.enabled);
296
+ const enabledModels = rtConfig.models.filter((mod) => mod.enabled);
524
297
  if (!enabledModels.length) {
525
298
  await sendUserVisible(msg, 'No enabled runtime models. Use `idlehands models enable <id>` in CLI.').catch(() => { });
526
299
  return true;
@@ -528,10 +301,10 @@ export async function handleTextCommand(managed, msg, content, ctx) {
528
301
  const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import('discord.js');
529
302
  const rows = [];
530
303
  let currentRow = new ActionRowBuilder();
531
- for (const m of enabledModels) {
304
+ for (const mod of enabledModels) {
532
305
  const btn = new ButtonBuilder()
533
- .setCustomId(`model_switch:${m.id}`)
534
- .setLabel(m.display_name.slice(0, 80))
306
+ .setCustomId(`model_switch:${mod.id}`)
307
+ .setLabel(mod.display_name.slice(0, 80))
535
308
  .setStyle(ButtonStyle.Primary);
536
309
  currentRow.addComponents(btn);
537
310
  if (currentRow.components.length >= 5) {
@@ -594,17 +367,17 @@ export async function handleTextCommand(managed, msg, content, ctx) {
594
367
  const { loadRuntimes } = await import('../runtime/store.js');
595
368
  const rtConfig = await loadRuntimes();
596
369
  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(() => { });
370
+ const planResult = plan({ modelId, mode: 'live' }, rtConfig, active);
371
+ if (!planResult.ok) {
372
+ await sendUserVisible(msg, `❌ Plan failed: ${planResult.reason}`).catch(() => { });
600
373
  return true;
601
374
  }
602
- if (result.reuse) {
375
+ if (planResult.reuse) {
603
376
  await sendUserVisible(msg, '✅ Runtime already active and healthy.').catch(() => { });
604
377
  return true;
605
378
  }
606
- const statusMsg = await sendUserVisible(msg, `⏳ Switching to \`${result.model.display_name}\`...`).catch(() => null);
607
- const execResult = await execute(result, {
379
+ const statusMsg = await sendUserVisible(msg, `⏳ Switching to \`${planResult.model.display_name}\`...`).catch(() => null);
380
+ const execResult = await execute(planResult, {
608
381
  onStep: async (step, status) => {
609
382
  if (status === 'done' && statusMsg) {
610
383
  await statusMsg.edit(`⏳ ${step.description}... ✓`).catch(() => { });
@@ -617,10 +390,10 @@ export async function handleTextCommand(managed, msg, content, ctx) {
617
390
  });
618
391
  if (execResult.ok) {
619
392
  if (statusMsg) {
620
- await statusMsg.edit(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
393
+ await statusMsg.edit(`✅ Switched to \`${planResult.model.display_name}\``).catch(() => { });
621
394
  }
622
395
  else {
623
- await sendUserVisible(msg, `✅ Switched to \`${result.model.display_name}\``).catch(() => { });
396
+ await sendUserVisible(msg, `✅ Switched to \`${planResult.model.display_name}\``).catch(() => { });
624
397
  }
625
398
  }
626
399
  else {
@@ -648,186 +421,11 @@ const DISCORD_RATE_LIMIT_MS = 15_000;
648
421
  export async function handleDiscordAnton(managed, msg, content, ctx) {
649
422
  const { sendUserVisible } = ctx;
650
423
  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;
424
+ const m = managed;
757
425
  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
- });
426
+ const result = await antonCommand(m, args, (t) => { channel.send(t).catch(() => { }); }, DISCORD_RATE_LIMIT_MS);
427
+ const text = formatMarkdown(result);
428
+ if (text)
429
+ await sendUserVisible(msg, text).catch(() => { });
832
430
  }
833
431
  //# sourceMappingURL=discord-commands.js.map