@visorcraft/idlehands 1.3.5 → 1.3.7
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.
- package/dist/anton/controller.js +17 -5
- package/dist/anton/controller.js.map +1 -1
- package/dist/bot/command-format.js +56 -0
- package/dist/bot/command-format.js.map +1 -0
- package/dist/bot/command-logic.js +651 -0
- package/dist/bot/command-logic.js.map +1 -0
- package/dist/bot/commands.js +77 -553
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord-commands.js +77 -479
- package/dist/bot/discord-commands.js.map +1 -1
- package/dist/bot/discord.js +1 -57
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/session-manager.js +8 -44
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/turn-lifecycle.js +66 -0
- package/dist/bot/turn-lifecycle.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
134
|
+
await send(approvalShowCommand(m, approvalMode));
|
|
169
135
|
return true;
|
|
170
136
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
473
|
-
const redacted = redactConfig(
|
|
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
|
|
496
|
-
const redacted = redactConfig(
|
|
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
|
|
519
|
-
if (!
|
|
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 =
|
|
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
|
|
304
|
+
for (const mod of enabledModels) {
|
|
532
305
|
const btn = new ButtonBuilder()
|
|
533
|
-
.setCustomId(`model_switch:${
|
|
534
|
-
.setLabel(
|
|
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
|
|
598
|
-
if (!
|
|
599
|
-
await sendUserVisible(msg, `❌ Plan failed: ${
|
|
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 (
|
|
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 \`${
|
|
607
|
-
const execResult = await execute(
|
|
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 \`${
|
|
393
|
+
await statusMsg.edit(`✅ Switched to \`${planResult.model.display_name}\``).catch(() => { });
|
|
621
394
|
}
|
|
622
395
|
else {
|
|
623
|
-
await sendUserVisible(msg, `✅ Switched to \`${
|
|
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
|
|
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
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|