@visorcraft/idlehands 1.3.4 → 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.
- package/dist/agent/formatting.js +1 -5
- package/dist/agent/formatting.js.map +1 -1
- package/dist/agent.js +114 -26
- package/dist/agent.js.map +1 -1
- package/dist/anton/reporter.js +2 -20
- package/dist/anton/reporter.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 +100 -552
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord-commands.js +431 -0
- package/dist/bot/discord-commands.js.map +1 -0
- package/dist/bot/discord-routing.js +1 -8
- package/dist/bot/discord-routing.js.map +1 -1
- package/dist/bot/discord.js +16 -870
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/session-manager.js +60 -44
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/telegram-commands.js +201 -0
- package/dist/bot/telegram-commands.js.map +1 -0
- package/dist/bot/telegram.js +10 -309
- package/dist/bot/telegram.js.map +1 -1
- package/dist/bot/turn-lifecycle.js +66 -0
- package/dist/bot/turn-lifecycle.js.map +1 -0
- package/dist/cli/commands/project.js +52 -0
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/context.js +1 -3
- package/dist/context.js.map +1 -1
- package/dist/progress/ir.js +0 -3
- package/dist/progress/ir.js.map +1 -1
- package/dist/progress/tool-summary.js +1 -4
- package/dist/progress/tool-summary.js.map +1 -1
- package/dist/progress/turn-progress.js +1 -5
- package/dist/progress/turn-progress.js.map +1 -1
- package/dist/runtime/executor.js +1 -3
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/health.js +2 -1
- package/dist/runtime/health.js.map +1 -1
- package/dist/shared/async.js +5 -0
- package/dist/shared/async.js.map +1 -0
- package/dist/shared/config-utils.js +8 -0
- package/dist/shared/config-utils.js.map +1 -0
- package/dist/shared/format.js +19 -0
- package/dist/shared/format.js.map +1 -0
- package/dist/shared/math.js +5 -0
- package/dist/shared/math.js.map +1 -0
- package/dist/shared/strings.js +8 -0
- package/dist/shared/strings.js.map +1 -0
- package/dist/tools/patch.js +82 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/path-safety.js +89 -0
- package/dist/tools/path-safety.js.map +1 -0
- package/dist/tools/undo.js +141 -0
- package/dist/tools/undo.js.map +1 -0
- package/dist/tools.js +11 -289
- package/dist/tools.js.map +1 -1
- package/dist/tui/event-bridge.js +1 -3
- package/dist/tui/event-bridge.js.map +1 -1
- package/dist/tui/render.js +1 -5
- package/dist/tui/render.js.map +1 -1
- package/dist/vault.js +1 -5
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
package/dist/bot/commands.js
CHANGED
|
@@ -1,70 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram bot command handlers.
|
|
3
3
|
* Each handler receives the grammy Context and the SessionManager.
|
|
4
|
+
*
|
|
5
|
+
* Business logic lives in command-logic.ts; this file is a thin wrapper
|
|
6
|
+
* that maps grammy Context → shared logic → HTML reply.
|
|
4
7
|
*/
|
|
5
|
-
import fs from 'node:fs/promises';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { runAnton } from '../anton/controller.js';
|
|
8
|
-
import { parseTaskFile } from '../anton/parser.js';
|
|
9
|
-
import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip, formatToolLoopEvent, formatCompactionEvent, formatVerificationDetail, } from '../anton/reporter.js';
|
|
10
8
|
import { firstToken } from '../cli/command-utils.js';
|
|
11
|
-
import {
|
|
9
|
+
import { formatHtml } from './command-format.js';
|
|
10
|
+
import { versionCommand, startCommand, helpCommand, modelCommand, compactCommand, statusCommand, watchdogCommand, dirShowCommand, approvalShowCommand, approvalSetCommand, modeShowCommand, modeSetCommand, subagentsShowCommand, subagentsSetCommand, changesCommand, undoCommand, vaultCommand, agentCommand, agentsCommand, escalateShowCommand, escalateSetCommand, deescalateCommand, gitStatusCommand, antonCommand, } from './command-logic.js';
|
|
12
11
|
import { escapeHtml } from './format.js';
|
|
12
|
+
/** Send formatted CmdResult as Telegram HTML. */
|
|
13
|
+
async function reply(ctx, result) {
|
|
14
|
+
const text = formatHtml(result);
|
|
15
|
+
if (!text)
|
|
16
|
+
return;
|
|
17
|
+
await ctx.reply(text, { parse_mode: 'HTML' });
|
|
18
|
+
}
|
|
13
19
|
export async function handleVersion({ ctx, botConfig }) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
20
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
20
|
+
await reply(ctx, versionCommand({
|
|
21
|
+
version: botConfig.version,
|
|
22
|
+
model: botConfig.model,
|
|
23
|
+
endpoint: botConfig.endpoint,
|
|
24
|
+
}));
|
|
21
25
|
}
|
|
22
26
|
export async function handleStart({ ctx, botConfig }) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'',
|
|
30
|
-
'Send me a coding task, or use /help for commands.',
|
|
31
|
-
];
|
|
32
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
27
|
+
await reply(ctx, startCommand({
|
|
28
|
+
version: botConfig.version,
|
|
29
|
+
model: botConfig.model,
|
|
30
|
+
endpoint: botConfig.endpoint,
|
|
31
|
+
defaultDir: botConfig.defaultDir,
|
|
32
|
+
}));
|
|
33
33
|
}
|
|
34
34
|
export async function handleHelp({ ctx }) {
|
|
35
|
-
|
|
36
|
-
'<b>Commands:</b>',
|
|
37
|
-
'',
|
|
38
|
-
'/start — Welcome + config summary',
|
|
39
|
-
'/help — This message',
|
|
40
|
-
'/new — Start a new session',
|
|
41
|
-
'/cancel — Abort current generation',
|
|
42
|
-
'/status — Session stats',
|
|
43
|
-
'/watchdog [status] — Show active watchdog settings',
|
|
44
|
-
'/agent — Show current agent info',
|
|
45
|
-
'/agents — List all configured agents',
|
|
46
|
-
'/escalate [model] — Use larger model for next message',
|
|
47
|
-
'/deescalate — Return to base model',
|
|
48
|
-
'/dir [path] — Get/set working directory',
|
|
49
|
-
'/pin — Pin current working directory',
|
|
50
|
-
'/model — Show current model',
|
|
51
|
-
'/approval [mode] — Get/set approval mode',
|
|
52
|
-
'/mode [code|sys] — Get/set mode',
|
|
53
|
-
'/compact — Trigger context compaction',
|
|
54
|
-
'/changes — Show files modified this session',
|
|
55
|
-
'/undo — Undo last edit',
|
|
56
|
-
'/subagents [on|off] — Toggle sub-agent delegation',
|
|
57
|
-
'/vault [query] — Search vault entries',
|
|
58
|
-
'/anton <file> — Start autonomous task runner',
|
|
59
|
-
'/anton status — Show task runner progress',
|
|
60
|
-
'/anton stop — Stop task runner',
|
|
61
|
-
'/anton last — Show last run results',
|
|
62
|
-
'/git_status — Show git status for working directory',
|
|
63
|
-
'/restart_bot — Restart the bot service',
|
|
64
|
-
'',
|
|
65
|
-
'Or just send any text as a coding task.',
|
|
66
|
-
];
|
|
67
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
35
|
+
await reply(ctx, helpCommand('telegram'));
|
|
68
36
|
}
|
|
69
37
|
export async function handleNew({ ctx, sessions }) {
|
|
70
38
|
const chatId = ctx.chat?.id;
|
|
@@ -89,24 +57,7 @@ export async function handleStatus({ ctx, sessions }) {
|
|
|
89
57
|
await ctx.reply('No active session. Send a message to start one.');
|
|
90
58
|
return;
|
|
91
59
|
}
|
|
92
|
-
|
|
93
|
-
const contextPct = s.contextWindow > 0
|
|
94
|
-
? Math.min(100, (s.currentContextTokens / s.contextWindow) * 100).toFixed(1)
|
|
95
|
-
: '?';
|
|
96
|
-
const lines = [
|
|
97
|
-
'<b>Session Status</b>',
|
|
98
|
-
'',
|
|
99
|
-
`<b>Model:</b> <code>${escapeHtml(s.model)}</code>`,
|
|
100
|
-
`<b>Harness:</b> <code>${escapeHtml(s.harness)}</code>`,
|
|
101
|
-
`<b>Dir:</b> <code>${escapeHtml(managed.workingDir)}</code>`,
|
|
102
|
-
`<b>Dir pinned:</b> ${managed.dirPinned ? 'yes' : 'no'}`,
|
|
103
|
-
`<b>Context:</b> ~${s.currentContextTokens.toLocaleString()} / ${s.contextWindow.toLocaleString()} (${contextPct}%)`,
|
|
104
|
-
`<b>Tokens:</b> prompt=${s.usage.prompt.toLocaleString()}, completion=${s.usage.completion.toLocaleString()}`,
|
|
105
|
-
`<b>In-flight:</b> ${managed.inFlight ? 'yes' : 'no'}`,
|
|
106
|
-
`<b>State:</b> ${managed.state}`,
|
|
107
|
-
`<b>Queue:</b> ${managed.pendingQueue.length} pending`,
|
|
108
|
-
];
|
|
109
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
60
|
+
await reply(ctx, statusCommand(managed));
|
|
110
61
|
}
|
|
111
62
|
export async function handleWatchdog({ ctx, sessions, botConfig }) {
|
|
112
63
|
const chatId = ctx.chat?.id;
|
|
@@ -122,34 +73,7 @@ export async function handleWatchdog({ ctx, sessions, botConfig }) {
|
|
|
122
73
|
return;
|
|
123
74
|
}
|
|
124
75
|
const managed = sessions.get(chatId);
|
|
125
|
-
|
|
126
|
-
const lines = [
|
|
127
|
-
'<b>Watchdog Status</b>',
|
|
128
|
-
'',
|
|
129
|
-
`<b>Timeout:</b> ${cfg.timeoutMs.toLocaleString()} ms (${Math.round(cfg.timeoutMs / 1000)}s)`,
|
|
130
|
-
`<b>Max compactions:</b> ${cfg.maxCompactions}`,
|
|
131
|
-
`<b>Grace windows:</b> ${cfg.idleGraceTimeouts}`,
|
|
132
|
-
`<b>Debug abort reason:</b> ${cfg.debugAbortReason ? 'on' : 'off'}`,
|
|
133
|
-
];
|
|
134
|
-
if (shouldRecommendWatchdogTuning(cfg)) {
|
|
135
|
-
lines.push('');
|
|
136
|
-
lines.push(`<b>Recommended tuning:</b> ${escapeHtml(WATCHDOG_RECOMMENDED_TUNING_TEXT)}`);
|
|
137
|
-
}
|
|
138
|
-
if (managed) {
|
|
139
|
-
const idleSec = managed.lastProgressAt > 0
|
|
140
|
-
? ((Date.now() - managed.lastProgressAt) / 1000).toFixed(1)
|
|
141
|
-
: 'n/a';
|
|
142
|
-
lines.push('');
|
|
143
|
-
lines.push(`<b>In-flight:</b> ${managed.inFlight ? 'yes' : 'no'}`);
|
|
144
|
-
lines.push(`<b>State:</b> ${escapeHtml(managed.state)}`);
|
|
145
|
-
lines.push(`<b>Compaction attempts (turn):</b> ${managed.watchdogCompactAttempts}`);
|
|
146
|
-
lines.push(`<b>Idle since progress:</b> ${escapeHtml(idleSec)}s`);
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
lines.push('');
|
|
150
|
-
lines.push('No active session yet. Send a message to start one.');
|
|
151
|
-
}
|
|
152
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
76
|
+
await reply(ctx, watchdogCommand(managed, botConfig.watchdog));
|
|
153
77
|
}
|
|
154
78
|
export async function handleDir({ ctx, sessions }) {
|
|
155
79
|
const chatId = ctx.chat?.id;
|
|
@@ -159,21 +83,7 @@ export async function handleDir({ ctx, sessions }) {
|
|
|
159
83
|
const arg = text.replace(/^\/dir\s*/, '').trim();
|
|
160
84
|
const managed = sessions.get(chatId);
|
|
161
85
|
if (!arg) {
|
|
162
|
-
|
|
163
|
-
const dir = managed?.workingDir ?? '(no session)';
|
|
164
|
-
const lines = [`<b>Working directory:</b> <code>${escapeHtml(dir)}</code>`];
|
|
165
|
-
if (managed) {
|
|
166
|
-
lines.push(`<b>Directory pinned:</b> ${managed.dirPinned ? 'yes' : 'no'}`);
|
|
167
|
-
if (!managed.dirPinned && managed.repoCandidates.length > 1) {
|
|
168
|
-
lines.push('<b>Action required:</b> run <code>/dir <repo-root></code> before file edits.');
|
|
169
|
-
const preview = managed.repoCandidates
|
|
170
|
-
.slice(0, 5)
|
|
171
|
-
.map((p) => `<code>${escapeHtml(p)}</code>`)
|
|
172
|
-
.join(', ');
|
|
173
|
-
lines.push(`<b>Detected repos:</b> ${preview}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
86
|
+
await reply(ctx, dirShowCommand(managed));
|
|
177
87
|
return;
|
|
178
88
|
}
|
|
179
89
|
// Set new dir
|
|
@@ -203,7 +113,6 @@ export async function handlePin({ ctx, sessions }) {
|
|
|
203
113
|
await ctx.reply('No working directory set. Use /dir to set one first.');
|
|
204
114
|
return;
|
|
205
115
|
}
|
|
206
|
-
// Re-use setDir logic to pin the current directory
|
|
207
116
|
const ok = await sessions.setDir(chatId, currentDir);
|
|
208
117
|
if (ok) {
|
|
209
118
|
await ctx.reply(`✅ Working directory pinned to <code>${escapeHtml(currentDir)}</code>`, {
|
|
@@ -214,6 +123,29 @@ export async function handlePin({ ctx, sessions }) {
|
|
|
214
123
|
await ctx.reply('❌ Directory not allowed or session error. Check bot.telegram.allowed_dirs / persona.allowed_dirs.');
|
|
215
124
|
}
|
|
216
125
|
}
|
|
126
|
+
export async function handleUnpin({ ctx, sessions }) {
|
|
127
|
+
const chatId = ctx.chat?.id;
|
|
128
|
+
if (!chatId)
|
|
129
|
+
return;
|
|
130
|
+
const managed = sessions.get(chatId);
|
|
131
|
+
if (!managed) {
|
|
132
|
+
await ctx.reply('No active session. Send a message to start one.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!managed.dirPinned) {
|
|
136
|
+
await ctx.reply('Directory is not pinned.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const ok = await sessions.unpin(chatId);
|
|
140
|
+
if (ok) {
|
|
141
|
+
await ctx.reply(`✅ Directory unpinned. Working directory remains at <code>${escapeHtml(managed.workingDir)}</code>`, {
|
|
142
|
+
parse_mode: 'HTML',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
await ctx.reply('❌ Failed to unpin directory.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
217
149
|
export async function handleModel({ ctx, sessions }) {
|
|
218
150
|
const chatId = ctx.chat?.id;
|
|
219
151
|
if (!chatId)
|
|
@@ -223,7 +155,7 @@ export async function handleModel({ ctx, sessions }) {
|
|
|
223
155
|
await ctx.reply('No active session. Send a message to start one.');
|
|
224
156
|
return;
|
|
225
157
|
}
|
|
226
|
-
await
|
|
158
|
+
await reply(ctx, modelCommand(managed));
|
|
227
159
|
}
|
|
228
160
|
export async function handleCompact({ ctx, sessions }) {
|
|
229
161
|
const chatId = ctx.chat?.id;
|
|
@@ -234,9 +166,7 @@ export async function handleCompact({ ctx, sessions }) {
|
|
|
234
166
|
await ctx.reply('No active session.');
|
|
235
167
|
return;
|
|
236
168
|
}
|
|
237
|
-
|
|
238
|
-
managed.session.reset();
|
|
239
|
-
await ctx.reply('🗜 Session context compacted (reset to system prompt).');
|
|
169
|
+
await reply(ctx, compactCommand(managed));
|
|
240
170
|
}
|
|
241
171
|
export async function handleApproval({ ctx, sessions }) {
|
|
242
172
|
const chatId = ctx.chat?.id;
|
|
@@ -244,22 +174,28 @@ export async function handleApproval({ ctx, sessions }) {
|
|
|
244
174
|
return;
|
|
245
175
|
const text = ctx.message?.text ?? '';
|
|
246
176
|
const arg = text.replace(/^\/approval\s*/, '').trim();
|
|
247
|
-
const modes = ['plan', 'default', 'auto-edit', 'yolo'];
|
|
248
177
|
const managed = sessions.get(chatId);
|
|
249
178
|
if (!arg) {
|
|
250
|
-
|
|
251
|
-
|
|
179
|
+
if (!managed) {
|
|
180
|
+
await ctx.reply('No active session.');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await reply(ctx, approvalShowCommand(managed));
|
|
252
184
|
return;
|
|
253
185
|
}
|
|
186
|
+
if (managed) {
|
|
187
|
+
const result = approvalSetCommand(managed, arg);
|
|
188
|
+
if (result) {
|
|
189
|
+
await reply(ctx, result);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// If no managed session but arg given, still try to validate
|
|
194
|
+
const modes = ['plan', 'default', 'auto-edit', 'yolo'];
|
|
254
195
|
if (!modes.includes(arg)) {
|
|
255
196
|
await ctx.reply(`Invalid mode. Options: ${modes.join(', ')}`);
|
|
256
197
|
return;
|
|
257
198
|
}
|
|
258
|
-
if (managed) {
|
|
259
|
-
managed.approvalMode = arg;
|
|
260
|
-
managed.config.approval_mode = arg;
|
|
261
|
-
managed.config.no_confirm = arg === 'yolo';
|
|
262
|
-
}
|
|
263
199
|
await ctx.reply(`✅ Approval mode set to <code>${escapeHtml(arg)}</code>`, {
|
|
264
200
|
parse_mode: 'HTML',
|
|
265
201
|
});
|
|
@@ -279,21 +215,10 @@ export async function handleMode({ ctx, sessions }) {
|
|
|
279
215
|
return;
|
|
280
216
|
}
|
|
281
217
|
if (!arg) {
|
|
282
|
-
await
|
|
283
|
-
parse_mode: 'HTML',
|
|
284
|
-
});
|
|
218
|
+
await reply(ctx, modeShowCommand(managed));
|
|
285
219
|
return;
|
|
286
220
|
}
|
|
287
|
-
|
|
288
|
-
await ctx.reply('Invalid mode. Options: code, sys');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
managed.config.mode = arg;
|
|
292
|
-
if (arg === 'sys' && managed.config.approval_mode === 'auto-edit') {
|
|
293
|
-
managed.config.approval_mode = 'default';
|
|
294
|
-
managed.approvalMode = 'default';
|
|
295
|
-
}
|
|
296
|
-
await ctx.reply(`✅ Mode set to <code>${escapeHtml(arg)}</code>`, { parse_mode: 'HTML' });
|
|
221
|
+
await reply(ctx, modeSetCommand(managed, arg));
|
|
297
222
|
}
|
|
298
223
|
export async function handleSubAgents({ ctx, sessions }) {
|
|
299
224
|
const chatId = ctx.chat?.id;
|
|
@@ -309,18 +234,11 @@ export async function handleSubAgents({ ctx, sessions }) {
|
|
|
309
234
|
await ctx.reply('No active session. Send a message to start one.');
|
|
310
235
|
return;
|
|
311
236
|
}
|
|
312
|
-
const current = managed.config.sub_agents?.enabled !== false;
|
|
313
237
|
if (!arg) {
|
|
314
|
-
await
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (arg !== 'on' && arg !== 'off') {
|
|
318
|
-
await ctx.reply('Invalid value. Usage: /subagents on | off');
|
|
238
|
+
await reply(ctx, subagentsShowCommand(managed));
|
|
319
239
|
return;
|
|
320
240
|
}
|
|
321
|
-
|
|
322
|
-
managed.config.sub_agents = { ...(managed.config.sub_agents ?? {}), enabled };
|
|
323
|
-
await ctx.reply(`✅ Sub-agents <code>${enabled ? 'on' : 'off'}</code>${!enabled ? ' — spawn_task disabled for this session' : ''}`, { parse_mode: 'HTML' });
|
|
241
|
+
await reply(ctx, subagentsSetCommand(managed, arg));
|
|
324
242
|
}
|
|
325
243
|
export async function handleChanges({ ctx, sessions }) {
|
|
326
244
|
const chatId = ctx.chat?.id;
|
|
@@ -331,31 +249,7 @@ export async function handleChanges({ ctx, sessions }) {
|
|
|
331
249
|
await ctx.reply('No active session.');
|
|
332
250
|
return;
|
|
333
251
|
}
|
|
334
|
-
|
|
335
|
-
if (!replay) {
|
|
336
|
-
await ctx.reply('Replay is disabled. No change tracking available.');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
const checkpoints = await replay.list(50);
|
|
341
|
-
if (!checkpoints.length) {
|
|
342
|
-
await ctx.reply('No file changes this session.');
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
// Group by file path for diffstat
|
|
346
|
-
const byFile = new Map();
|
|
347
|
-
for (const cp of checkpoints) {
|
|
348
|
-
byFile.set(cp.filePath, (byFile.get(cp.filePath) ?? 0) + 1);
|
|
349
|
-
}
|
|
350
|
-
const lines = [`<b>Session changes (${byFile.size} files):</b>`, ''];
|
|
351
|
-
for (const [fp, count] of byFile) {
|
|
352
|
-
lines.push(` ✎ <code>${escapeHtml(fp)}</code> (${count} edit${count > 1 ? 's' : ''})`);
|
|
353
|
-
}
|
|
354
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
355
|
-
}
|
|
356
|
-
catch (e) {
|
|
357
|
-
await ctx.reply(`Error listing changes: ${e?.message ?? e}`);
|
|
358
|
-
}
|
|
252
|
+
await reply(ctx, await changesCommand(managed));
|
|
359
253
|
}
|
|
360
254
|
export async function handleUndo({ ctx, sessions }) {
|
|
361
255
|
const chatId = ctx.chat?.id;
|
|
@@ -366,25 +260,7 @@ export async function handleUndo({ ctx, sessions }) {
|
|
|
366
260
|
await ctx.reply('No active session.');
|
|
367
261
|
return;
|
|
368
262
|
}
|
|
369
|
-
|
|
370
|
-
if (!lastPath) {
|
|
371
|
-
await ctx.reply('No recent edits to undo.');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
try {
|
|
375
|
-
// Use the undo_path tool function
|
|
376
|
-
const { undo_path } = await import('../tools.js');
|
|
377
|
-
const ctx2 = {
|
|
378
|
-
cwd: managed.workingDir,
|
|
379
|
-
noConfirm: true,
|
|
380
|
-
dryRun: false,
|
|
381
|
-
};
|
|
382
|
-
const result = await undo_path(ctx2, { path: lastPath });
|
|
383
|
-
await ctx.reply(`✅ ${result}`);
|
|
384
|
-
}
|
|
385
|
-
catch (e) {
|
|
386
|
-
await ctx.reply(`❌ Undo failed: ${e?.message ?? e}`);
|
|
387
|
-
}
|
|
263
|
+
await reply(ctx, await undoCommand(managed));
|
|
388
264
|
}
|
|
389
265
|
export async function handleVault({ ctx, sessions }) {
|
|
390
266
|
const chatId = ctx.chat?.id;
|
|
@@ -395,37 +271,12 @@ export async function handleVault({ ctx, sessions }) {
|
|
|
395
271
|
await ctx.reply('No active session.');
|
|
396
272
|
return;
|
|
397
273
|
}
|
|
398
|
-
const vault = managed.session.vault;
|
|
399
|
-
if (!vault) {
|
|
400
|
-
await ctx.reply('Vault is disabled.');
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
274
|
const text = ctx.message?.text ?? '';
|
|
404
275
|
const query = text.replace(/^\/vault\s*/, '').trim();
|
|
405
|
-
|
|
406
|
-
await ctx.reply('Usage: /vault <search query>', { parse_mode: 'HTML' });
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
try {
|
|
410
|
-
const results = await vault.search(query, 5);
|
|
411
|
-
if (!results.length) {
|
|
412
|
-
await ctx.reply(`No vault results for "${escapeHtml(query)}"`, { parse_mode: 'HTML' });
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const lines = [`<b>Vault results for "${escapeHtml(query)}":</b>`, ''];
|
|
416
|
-
for (const r of results) {
|
|
417
|
-
const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || '?'}`;
|
|
418
|
-
const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 120);
|
|
419
|
-
lines.push(`• <b>${escapeHtml(title)}</b>: ${escapeHtml(body)}`);
|
|
420
|
-
}
|
|
421
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
422
|
-
}
|
|
423
|
-
catch (e) {
|
|
424
|
-
await ctx.reply(`Error searching vault: ${e?.message ?? e}`);
|
|
425
|
-
}
|
|
276
|
+
await reply(ctx, await vaultCommand(managed, query));
|
|
426
277
|
}
|
|
427
278
|
// ── Anton ───────────────────────────────────────────────────────────
|
|
428
|
-
const ANTON_RATE_LIMIT_MS = 10_000;
|
|
279
|
+
const ANTON_RATE_LIMIT_MS = 10_000;
|
|
429
280
|
export async function handleAnton({ ctx, sessions }) {
|
|
430
281
|
const chatId = ctx.chat?.id;
|
|
431
282
|
const userId = ctx.from?.id;
|
|
@@ -434,209 +285,23 @@ export async function handleAnton({ ctx, sessions }) {
|
|
|
434
285
|
const text = ctx.message?.text ?? '';
|
|
435
286
|
const args = text.replace(/^\/anton\s*/, '').trim();
|
|
436
287
|
const sub = firstToken(args);
|
|
437
|
-
|
|
438
|
-
// status
|
|
439
|
-
if (!sub || sub === 'status') {
|
|
440
|
-
if (!managed
|
|
441
|
-
await ctx.reply('No
|
|
288
|
+
let managed = sessions.get(chatId);
|
|
289
|
+
// For status/stop/last we need an existing session
|
|
290
|
+
if (!sub || sub === 'status' || sub === 'stop' || sub === 'last') {
|
|
291
|
+
if (!managed) {
|
|
292
|
+
await ctx.reply('No active session.');
|
|
442
293
|
return;
|
|
443
294
|
}
|
|
444
|
-
|
|
445
|
-
await ctx.reply('🛑 Anton is stopping. Please wait for the current attempt to unwind.');
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (managed.antonProgress) {
|
|
449
|
-
const line1 = formatProgressBar(managed.antonProgress);
|
|
450
|
-
if (managed.antonProgress.currentTask) {
|
|
451
|
-
await ctx.reply(`${line1}\n\n<b>Working on:</b> <i>${escapeHtml(managed.antonProgress.currentTask)}</i> (Attempt ${managed.antonProgress.currentAttempt})`, { parse_mode: 'HTML' });
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
await ctx.reply(line1);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
await ctx.reply('🤖 Anton is running (no progress data yet).');
|
|
459
|
-
}
|
|
295
|
+
await reply(ctx, await antonCommand(managed, args, (t) => { ctx.reply(t).catch(() => { }); }, ANTON_RATE_LIMIT_MS));
|
|
460
296
|
return;
|
|
461
297
|
}
|
|
462
|
-
//
|
|
463
|
-
if (sub === 'stop') {
|
|
464
|
-
if (!managed?.antonActive || !managed.antonAbortSignal) {
|
|
465
|
-
await ctx.reply('No Anton run in progress.');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
managed.lastActivity = Date.now();
|
|
469
|
-
managed.antonAbortSignal.aborted = true;
|
|
470
|
-
await ctx.reply('🛑 Anton stop requested. Run will halt after the current task.');
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
// last
|
|
474
|
-
if (sub === 'last') {
|
|
475
|
-
if (!managed?.antonLastResult) {
|
|
476
|
-
await ctx.reply('No previous Anton run.');
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
await ctx.reply(formatRunSummary(managed.antonLastResult));
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
// start run — args is the file path (possibly with "run" prefix)
|
|
483
|
-
const filePart = sub === 'run' ? args.replace(/^\S+\s*/, '').trim() : args;
|
|
484
|
-
if (!filePart) {
|
|
485
|
-
await ctx.reply([
|
|
486
|
-
'<b>/anton</b> — Autonomous task runner',
|
|
487
|
-
'',
|
|
488
|
-
'/anton <file> — Start run',
|
|
489
|
-
'/anton status — Show progress',
|
|
490
|
-
'/anton stop — Stop running',
|
|
491
|
-
'/anton last — Last run results',
|
|
492
|
-
].join('\n'), { parse_mode: 'HTML' });
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
// Ensure session exists
|
|
298
|
+
// For start — ensure session exists
|
|
496
299
|
const session = managed || (await sessions.getOrCreate(chatId, userId));
|
|
497
300
|
if (!session) {
|
|
498
301
|
await ctx.reply('⚠️ Too many active sessions. Try again later (or wait for an old session to expire).');
|
|
499
302
|
return;
|
|
500
303
|
}
|
|
501
|
-
|
|
502
|
-
const staleMs = Date.now() - session.lastActivity;
|
|
503
|
-
if (staleMs > 120_000) {
|
|
504
|
-
session.antonActive = false;
|
|
505
|
-
session.antonAbortSignal = null;
|
|
506
|
-
session.antonProgress = null;
|
|
507
|
-
await ctx.reply('♻️ Recovered stale Anton run state. Starting a fresh run...');
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
const msg = session.antonAbortSignal?.aborted
|
|
511
|
-
? '🛑 Anton is still stopping. Please wait a moment, then try again.'
|
|
512
|
-
: '⚠️ Anton is already running. Use /anton stop first.';
|
|
513
|
-
await ctx.reply(msg);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const cwd = session.workingDir;
|
|
518
|
-
const filePath = path.resolve(cwd, filePart);
|
|
519
|
-
try {
|
|
520
|
-
await fs.stat(filePath);
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
await ctx.reply(`File not found: ${escapeHtml(filePath)}`, { parse_mode: 'HTML' });
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const defaults = session.config.anton || {};
|
|
527
|
-
const runConfig = {
|
|
528
|
-
taskFile: filePath,
|
|
529
|
-
projectDir: cwd,
|
|
530
|
-
maxRetriesPerTask: defaults.max_retries ?? 3,
|
|
531
|
-
maxIterations: defaults.max_iterations ?? 200,
|
|
532
|
-
taskMaxIterations: defaults.task_max_iterations ?? 50,
|
|
533
|
-
taskTimeoutSec: defaults.task_timeout_sec ?? 600,
|
|
534
|
-
totalTimeoutSec: defaults.total_timeout_sec ?? 7200,
|
|
535
|
-
maxTotalTokens: defaults.max_total_tokens ?? Infinity,
|
|
536
|
-
maxPromptTokensPerAttempt: defaults.max_prompt_tokens_per_attempt ?? 128_000,
|
|
537
|
-
autoCommit: defaults.auto_commit ?? true,
|
|
538
|
-
branch: false,
|
|
539
|
-
allowDirty: false,
|
|
540
|
-
aggressiveCleanOnFail: false,
|
|
541
|
-
verifyAi: defaults.verify_ai ?? true,
|
|
542
|
-
verifyModel: undefined,
|
|
543
|
-
decompose: defaults.decompose ?? true,
|
|
544
|
-
maxDecomposeDepth: defaults.max_decompose_depth ?? 2,
|
|
545
|
-
maxTotalTasks: defaults.max_total_tasks ?? 500,
|
|
546
|
-
buildCommand: undefined,
|
|
547
|
-
testCommand: undefined,
|
|
548
|
-
lintCommand: undefined,
|
|
549
|
-
skipOnFail: defaults.skip_on_fail ?? false,
|
|
550
|
-
skipOnBlocked: defaults.skip_on_blocked ?? true,
|
|
551
|
-
rollbackOnFail: defaults.rollback_on_fail ?? false,
|
|
552
|
-
maxIdenticalFailures: defaults.max_identical_failures ?? 5,
|
|
553
|
-
approvalMode: (defaults.approval_mode ?? 'yolo'),
|
|
554
|
-
verbose: false,
|
|
555
|
-
dryRun: false,
|
|
556
|
-
};
|
|
557
|
-
const abortSignal = { aborted: false };
|
|
558
|
-
session.antonActive = true;
|
|
559
|
-
session.antonAbortSignal = abortSignal;
|
|
560
|
-
session.antonProgress = null;
|
|
561
|
-
let lastProgressAt = 0;
|
|
562
|
-
const progress = {
|
|
563
|
-
onTaskStart(task, attempt, prog) {
|
|
564
|
-
session.antonProgress = prog;
|
|
565
|
-
session.lastActivity = Date.now();
|
|
566
|
-
const now = Date.now();
|
|
567
|
-
if (now - lastProgressAt >= ANTON_RATE_LIMIT_MS) {
|
|
568
|
-
lastProgressAt = now;
|
|
569
|
-
ctx.reply(formatTaskStart(task, attempt, prog)).catch(() => { });
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
onTaskEnd(task, result, prog) {
|
|
573
|
-
session.antonProgress = prog;
|
|
574
|
-
session.lastActivity = Date.now();
|
|
575
|
-
const now = Date.now();
|
|
576
|
-
if (now - lastProgressAt >= ANTON_RATE_LIMIT_MS) {
|
|
577
|
-
lastProgressAt = now;
|
|
578
|
-
ctx.reply(formatTaskEnd(task, result, prog)).catch(() => { });
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
onTaskSkip(task, reason) {
|
|
582
|
-
session.lastActivity = Date.now();
|
|
583
|
-
ctx.reply(formatTaskSkip(task, reason)).catch(() => { });
|
|
584
|
-
},
|
|
585
|
-
onRunComplete(result) {
|
|
586
|
-
session.lastActivity = Date.now();
|
|
587
|
-
session.antonLastResult = result;
|
|
588
|
-
session.antonActive = false;
|
|
589
|
-
session.antonAbortSignal = null;
|
|
590
|
-
session.antonProgress = null;
|
|
591
|
-
ctx.reply(formatRunSummary(result)).catch(() => { });
|
|
592
|
-
},
|
|
593
|
-
onHeartbeat() {
|
|
594
|
-
session.lastActivity = Date.now();
|
|
595
|
-
},
|
|
596
|
-
onToolLoop(taskText, event) {
|
|
597
|
-
session.lastActivity = Date.now();
|
|
598
|
-
if (defaults.progress_events !== false) {
|
|
599
|
-
ctx.reply(formatToolLoopEvent(taskText, event)).catch(() => { });
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
onCompaction(taskText, event) {
|
|
603
|
-
session.lastActivity = Date.now();
|
|
604
|
-
if (defaults.progress_events !== false && event.droppedMessages >= 5) {
|
|
605
|
-
ctx.reply(formatCompactionEvent(taskText, event)).catch(() => { });
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
onVerification(taskText, verification) {
|
|
609
|
-
session.lastActivity = Date.now();
|
|
610
|
-
if (defaults.progress_events !== false && !verification.passed) {
|
|
611
|
-
ctx.reply(formatVerificationDetail(taskText, verification)).catch(() => { });
|
|
612
|
-
}
|
|
613
|
-
},
|
|
614
|
-
};
|
|
615
|
-
let pendingCount = 0;
|
|
616
|
-
try {
|
|
617
|
-
const tf = await parseTaskFile(filePath);
|
|
618
|
-
pendingCount = tf.pending.length;
|
|
619
|
-
}
|
|
620
|
-
catch {
|
|
621
|
-
/* non-fatal */
|
|
622
|
-
}
|
|
623
|
-
await ctx.reply(`🤖 Anton started on ${escapeHtml(filePart)} (${pendingCount} tasks pending)`, {
|
|
624
|
-
parse_mode: 'HTML',
|
|
625
|
-
});
|
|
626
|
-
runAnton({
|
|
627
|
-
config: runConfig,
|
|
628
|
-
idlehandsConfig: session.config,
|
|
629
|
-
progress,
|
|
630
|
-
abortSignal,
|
|
631
|
-
vault: session.session.vault,
|
|
632
|
-
lens: session.session.lens,
|
|
633
|
-
}).catch((err) => {
|
|
634
|
-
session.lastActivity = Date.now();
|
|
635
|
-
session.antonActive = false;
|
|
636
|
-
session.antonAbortSignal = null;
|
|
637
|
-
session.antonProgress = null;
|
|
638
|
-
ctx.reply(`Anton error: ${err.message}`).catch(() => { });
|
|
639
|
-
});
|
|
304
|
+
await reply(ctx, await antonCommand(session, args, (t) => { ctx.reply(t).catch(() => { }); }, ANTON_RATE_LIMIT_MS));
|
|
640
305
|
}
|
|
641
306
|
// ── Multi-agent commands ───────────────────────────────────────────────
|
|
642
307
|
export async function handleAgent({ ctx, sessions, botConfig: _botConfig, }) {
|
|
@@ -644,71 +309,25 @@ export async function handleAgent({ ctx, sessions, botConfig: _botConfig, }) {
|
|
|
644
309
|
if (!chatId)
|
|
645
310
|
return;
|
|
646
311
|
const managed = sessions.get(chatId);
|
|
647
|
-
if (!managed
|
|
312
|
+
if (!managed) {
|
|
648
313
|
await ctx.reply('No agent configured. Using global config.');
|
|
649
314
|
return;
|
|
650
315
|
}
|
|
651
|
-
|
|
652
|
-
const lines = [
|
|
653
|
-
`<b>Agent: ${escapeHtml(p.display_name || managed.agentId)}</b> (<code>${escapeHtml(managed.agentId)}</code>)`,
|
|
654
|
-
...(p.model ? [`<b>Model:</b> <code>${escapeHtml(p.model)}</code>`] : []),
|
|
655
|
-
...(p.endpoint ? [`<b>Endpoint:</b> <code>${escapeHtml(p.endpoint)}</code>`] : []),
|
|
656
|
-
...(p.approval_mode ? [`<b>Approval:</b> <code>${escapeHtml(p.approval_mode)}</code>`] : []),
|
|
657
|
-
...(p.default_dir ? [`<b>Default dir:</b> <code>${escapeHtml(p.default_dir)}</code>`] : []),
|
|
658
|
-
...(p.allowed_dirs?.length
|
|
659
|
-
? [
|
|
660
|
-
`<b>Allowed dirs:</b> ${p.allowed_dirs.map((d) => `<code>${escapeHtml(d)}</code>`).join(', ')}`,
|
|
661
|
-
]
|
|
662
|
-
: []),
|
|
663
|
-
];
|
|
664
|
-
// Show escalation info if configured
|
|
665
|
-
if (p.escalation?.models?.length) {
|
|
666
|
-
lines.push('');
|
|
667
|
-
lines.push(`<b>Escalation models:</b> ${p.escalation.models.map((m) => `<code>${escapeHtml(m)}</code>`).join(', ')}`);
|
|
668
|
-
if (managed.currentModelIndex > 0) {
|
|
669
|
-
lines.push(`<b>Current tier:</b> ${managed.currentModelIndex} (escalated)`);
|
|
670
|
-
}
|
|
671
|
-
if (managed.pendingEscalation) {
|
|
672
|
-
lines.push(`<b>Pending escalation:</b> <code>${escapeHtml(managed.pendingEscalation)}</code>`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
316
|
+
await reply(ctx, agentCommand(managed));
|
|
676
317
|
}
|
|
677
318
|
export async function handleAgents({ ctx, sessions, botConfig }) {
|
|
678
319
|
const chatId = ctx.chat?.id;
|
|
679
320
|
if (!chatId)
|
|
680
321
|
return;
|
|
681
|
-
const
|
|
682
|
-
if (!
|
|
322
|
+
const managed = sessions.get(chatId);
|
|
323
|
+
if (!managed) {
|
|
683
324
|
await ctx.reply('No agents configured. Using global config.');
|
|
684
325
|
return;
|
|
685
326
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const current = id === currentAgentId ? ' ← current' : '';
|
|
691
|
-
const model = agent.model ? ` (${escapeHtml(agent.model)})` : '';
|
|
692
|
-
lines.push(`• <b>${escapeHtml(agent.display_name || id)}</b> (<code>${escapeHtml(id)}</code>)${model}${current}`);
|
|
693
|
-
}
|
|
694
|
-
// Show routing rules
|
|
695
|
-
const routing = botConfig.telegram?.routing;
|
|
696
|
-
if (routing) {
|
|
697
|
-
lines.push('', '<b>Routing:</b>');
|
|
698
|
-
if (routing.default)
|
|
699
|
-
lines.push(`Default: <code>${escapeHtml(routing.default)}</code>`);
|
|
700
|
-
if (routing.users && Object.keys(routing.users).length > 0) {
|
|
701
|
-
lines.push(`Users: ${Object.entries(routing.users)
|
|
702
|
-
.map(([u, a]) => `${u}→${escapeHtml(a)}`)
|
|
703
|
-
.join(', ')}`);
|
|
704
|
-
}
|
|
705
|
-
if (routing.chats && Object.keys(routing.chats).length > 0) {
|
|
706
|
-
lines.push(`Chats: ${Object.entries(routing.chats)
|
|
707
|
-
.map(([c, a]) => `${c}→${escapeHtml(a)}`)
|
|
708
|
-
.join(', ')}`);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
327
|
+
await reply(ctx, agentsCommand(managed, {
|
|
328
|
+
agents: botConfig.telegram?.agents,
|
|
329
|
+
routing: botConfig.telegram?.routing,
|
|
330
|
+
}));
|
|
712
331
|
}
|
|
713
332
|
export async function handleEscalate({ ctx, sessions, botConfig }) {
|
|
714
333
|
const chatId = ctx.chat?.id;
|
|
@@ -720,50 +339,20 @@ export async function handleEscalate({ ctx, sessions, botConfig }) {
|
|
|
720
339
|
await ctx.reply('No active session. Send a message first.');
|
|
721
340
|
return;
|
|
722
341
|
}
|
|
723
|
-
const
|
|
342
|
+
const m = managed;
|
|
343
|
+
const escalation = m.agentPersona?.escalation;
|
|
724
344
|
if (!escalation || !escalation.models?.length) {
|
|
725
345
|
await ctx.reply('❌ No escalation models configured for this agent.');
|
|
726
346
|
return;
|
|
727
347
|
}
|
|
728
348
|
const text = ctx.message?.text ?? '';
|
|
729
349
|
const arg = text.replace(/^\/escalate\s*/, '').trim();
|
|
730
|
-
// No arg: show available models and current state
|
|
731
350
|
if (!arg) {
|
|
732
351
|
const currentModel = managed.config.model || botConfig.model || 'default';
|
|
733
|
-
|
|
734
|
-
`<b>Current model:</b> <code>${escapeHtml(currentModel)}</code>`,
|
|
735
|
-
`<b>Escalation models:</b> ${escalation.models.map((m) => `<code>${escapeHtml(m)}</code>`).join(', ')}`,
|
|
736
|
-
'',
|
|
737
|
-
'Usage: /escalate <model> or /escalate next',
|
|
738
|
-
'Then send your message - it will use the escalated model.',
|
|
739
|
-
];
|
|
740
|
-
if (managed.pendingEscalation) {
|
|
741
|
-
lines.push('', `⚡ <b>Pending escalation:</b> <code>${escapeHtml(managed.pendingEscalation)}</code> (next message will use this)`);
|
|
742
|
-
}
|
|
743
|
-
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
352
|
+
await reply(ctx, escalateShowCommand(m, currentModel));
|
|
744
353
|
return;
|
|
745
354
|
}
|
|
746
|
-
|
|
747
|
-
let targetModel;
|
|
748
|
-
let targetEndpoint;
|
|
749
|
-
if (arg.toLowerCase() === 'next') {
|
|
750
|
-
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
751
|
-
targetModel = escalation.models[nextIndex];
|
|
752
|
-
targetEndpoint = escalation.tiers?.[nextIndex]?.endpoint;
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
// Specific model requested
|
|
756
|
-
if (!escalation.models.includes(arg)) {
|
|
757
|
-
await ctx.reply(`❌ Model <code>${escapeHtml(arg)}</code> not in escalation chain. Available: ${escalation.models.map((m) => `<code>${escapeHtml(m)}</code>`).join(', ')}`, { parse_mode: 'HTML' });
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
targetModel = arg;
|
|
761
|
-
const idx = escalation.models.indexOf(arg);
|
|
762
|
-
targetEndpoint = escalation.tiers?.[idx]?.endpoint;
|
|
763
|
-
}
|
|
764
|
-
managed.pendingEscalation = targetModel;
|
|
765
|
-
managed.pendingEscalationEndpoint = targetEndpoint || null;
|
|
766
|
-
await ctx.reply(`⚡ Next message will use <code>${escapeHtml(targetModel)}</code>. Send your request now.`, { parse_mode: 'HTML' });
|
|
355
|
+
await reply(ctx, escalateSetCommand(m, arg));
|
|
767
356
|
}
|
|
768
357
|
export async function handleDeescalate({ ctx, sessions, botConfig, }) {
|
|
769
358
|
const chatId = ctx.chat?.id;
|
|
@@ -774,15 +363,13 @@ export async function handleDeescalate({ ctx, sessions, botConfig, }) {
|
|
|
774
363
|
await ctx.reply('No active session.');
|
|
775
364
|
return;
|
|
776
365
|
}
|
|
777
|
-
|
|
778
|
-
|
|
366
|
+
const m = managed;
|
|
367
|
+
const baseModel = m.agentPersona?.model || botConfig.model || 'default';
|
|
368
|
+
const result = deescalateCommand(m, baseModel);
|
|
369
|
+
if (result !== 'recreate') {
|
|
370
|
+
await reply(ctx, result);
|
|
779
371
|
return;
|
|
780
372
|
}
|
|
781
|
-
const baseModel = managed.agentPersona?.model || botConfig.model || 'default';
|
|
782
|
-
managed.pendingEscalation = null;
|
|
783
|
-
managed.pendingEscalationEndpoint = null;
|
|
784
|
-
managed.currentModelIndex = 0;
|
|
785
|
-
// Recreate session with base model
|
|
786
373
|
try {
|
|
787
374
|
await sessions.recreateSession(chatId, { model: baseModel });
|
|
788
375
|
await ctx.reply(`✅ Returned to base model: <code>${escapeHtml(baseModel)}</code>`, {
|
|
@@ -796,7 +383,6 @@ export async function handleDeescalate({ ctx, sessions, botConfig, }) {
|
|
|
796
383
|
export async function handleRestartBot({ ctx }) {
|
|
797
384
|
const { spawn } = await import('node:child_process');
|
|
798
385
|
await ctx.reply('🔄 Restarting idlehands-bot service...');
|
|
799
|
-
// Spawn detached process to restart after we return
|
|
800
386
|
spawn('systemctl', ['--user', 'restart', 'idlehands-bot'], {
|
|
801
387
|
detached: true,
|
|
802
388
|
stdio: 'ignore',
|
|
@@ -816,44 +402,6 @@ export async function handleGitStatus({ ctx, sessions }) {
|
|
|
816
402
|
await ctx.reply('No working directory set. Use /dir to set one.');
|
|
817
403
|
return;
|
|
818
404
|
}
|
|
819
|
-
|
|
820
|
-
// Run git status -s (short format)
|
|
821
|
-
const statusResult = spawnSync('git', ['status', '-s'], {
|
|
822
|
-
cwd,
|
|
823
|
-
encoding: 'utf8',
|
|
824
|
-
timeout: 5000,
|
|
825
|
-
});
|
|
826
|
-
if (statusResult.status !== 0) {
|
|
827
|
-
const err = String(statusResult.stderr || statusResult.error || 'Unknown error');
|
|
828
|
-
if (err.includes('not a git repository') || err.includes('not in a git')) {
|
|
829
|
-
await ctx.reply('❌ Not a git repository.');
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
await ctx.reply(`❌ git status failed: ${escapeHtml(err.slice(0, 200))}`);
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
const statusOut = String(statusResult.stdout || '').trim();
|
|
837
|
-
// Also get branch info
|
|
838
|
-
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
839
|
-
cwd,
|
|
840
|
-
encoding: 'utf8',
|
|
841
|
-
timeout: 2000,
|
|
842
|
-
});
|
|
843
|
-
const branch = branchResult.status === 0 ? String(branchResult.stdout || '').trim() : 'unknown';
|
|
844
|
-
if (!statusOut) {
|
|
845
|
-
await ctx.reply(`📁 <b>${escapeHtml(cwd)}</b>\n🌿 Branch: <code>${escapeHtml(branch)}</code>\n\n✅ Working tree clean`, { parse_mode: 'HTML' });
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const lines = statusOut.split('\n').slice(0, 30); // Limit to 30 lines
|
|
849
|
-
const truncated = statusOut.split('\n').length > 30;
|
|
850
|
-
const formatted = lines
|
|
851
|
-
.map((line) => {
|
|
852
|
-
const code = line.slice(0, 2);
|
|
853
|
-
const file = line.slice(3);
|
|
854
|
-
return `<code>${escapeHtml(code)}</code> ${escapeHtml(file)}`;
|
|
855
|
-
})
|
|
856
|
-
.join('\n');
|
|
857
|
-
await ctx.reply(`📁 <b>${escapeHtml(cwd)}</b>\n🌿 Branch: <code>${escapeHtml(branch)}</code>\n\n<pre>${formatted}${truncated ? '\n...' : ''}</pre>`, { parse_mode: 'HTML' });
|
|
405
|
+
await reply(ctx, await gitStatusCommand(cwd));
|
|
858
406
|
}
|
|
859
407
|
//# sourceMappingURL=commands.js.map
|