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