@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/discord.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { Client, Events, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder, } from 'discord.js';
|
|
4
3
|
import { createSession } from '../agent.js';
|
|
5
|
-
import { runAnton } from '../anton/controller.js';
|
|
6
|
-
import { parseTaskFile } from '../anton/parser.js';
|
|
7
|
-
import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip, formatToolLoopEvent, formatCompactionEvent, formatVerificationDetail, } from '../anton/reporter.js';
|
|
8
|
-
import { firstToken } from '../cli/command-utils.js';
|
|
9
4
|
import { chainAgentHooks } from '../progress/agent-hooks.js';
|
|
10
5
|
import { projectDir, PKG_VERSION } from '../utils.js';
|
|
11
6
|
import { WATCHDOG_RECOMMENDED_TUNING_TEXT, formatWatchdogCancelMessage, resolveWatchdogSettings, shouldRecommendWatchdogTuning, } from '../watchdog.js';
|
|
12
7
|
import { DiscordConfirmProvider } from './confirm-discord.js';
|
|
13
|
-
import { detectRepoCandidates, expandHome,
|
|
14
|
-
import { parseAllowedUsers, normalizeApprovalMode,
|
|
8
|
+
import { detectRepoCandidates, expandHome, normalizeAllowedDirs, } from './dir-guard.js';
|
|
9
|
+
import { parseAllowedUsers, normalizeApprovalMode, safeContent, detectEscalation, checkKeywordEscalation, resolveAgentForMessage, sessionKeyForMessage, } from './discord-routing.js';
|
|
15
10
|
import { DiscordStreamingMessage } from './discord-streaming.js';
|
|
16
11
|
import { isToolLoopBreak, formatAutoContinueNotice, AUTO_CONTINUE_PROMPT } from './auto-continue.js';
|
|
12
|
+
import { handleTextCommand } from './discord-commands.js';
|
|
13
|
+
import { beginTurn, isTurnActive, markProgress, finishTurn, cancelActive, } from './turn-lifecycle.js';
|
|
17
14
|
export async function startDiscordBot(config, botConfig) {
|
|
18
15
|
const token = process.env.IDLEHANDS_DISCORD_TOKEN || botConfig.token;
|
|
19
16
|
if (!token) {
|
|
@@ -210,63 +207,6 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
209
|
}, 60_000);
|
|
213
|
-
function beginTurn(managed) {
|
|
214
|
-
if (managed.inFlight || managed.state === 'resetting')
|
|
215
|
-
return null;
|
|
216
|
-
const controller = new AbortController();
|
|
217
|
-
managed.inFlight = true;
|
|
218
|
-
managed.state = 'running';
|
|
219
|
-
managed.activeTurnId += 1;
|
|
220
|
-
managed.activeAbortController = controller;
|
|
221
|
-
managed.lastProgressAt = Date.now();
|
|
222
|
-
managed.lastActivity = Date.now();
|
|
223
|
-
managed.watchdogCompactAttempts = 0;
|
|
224
|
-
return { turnId: managed.activeTurnId, controller };
|
|
225
|
-
}
|
|
226
|
-
function isTurnActive(managed, turnId) {
|
|
227
|
-
return managed.inFlight && managed.activeTurnId == turnId && managed.state !== 'resetting';
|
|
228
|
-
}
|
|
229
|
-
function markProgress(managed, turnId) {
|
|
230
|
-
if (managed.activeTurnId !== turnId)
|
|
231
|
-
return;
|
|
232
|
-
managed.lastProgressAt = Date.now();
|
|
233
|
-
managed.lastActivity = Date.now();
|
|
234
|
-
}
|
|
235
|
-
function finishTurn(managed, turnId) {
|
|
236
|
-
if (managed.activeTurnId !== turnId)
|
|
237
|
-
return;
|
|
238
|
-
managed.inFlight = false;
|
|
239
|
-
managed.state = 'idle';
|
|
240
|
-
managed.activeAbortController = null;
|
|
241
|
-
managed.lastActivity = Date.now();
|
|
242
|
-
}
|
|
243
|
-
function cancelActive(managed) {
|
|
244
|
-
const wasRunning = managed.inFlight;
|
|
245
|
-
const queueSize = managed.pendingQueue.length;
|
|
246
|
-
if (!wasRunning && queueSize === 0) {
|
|
247
|
-
return { ok: false, message: 'Nothing to cancel.' };
|
|
248
|
-
}
|
|
249
|
-
// Always clear queued work.
|
|
250
|
-
managed.pendingQueue = [];
|
|
251
|
-
if (wasRunning) {
|
|
252
|
-
managed.state = 'canceling';
|
|
253
|
-
try {
|
|
254
|
-
managed.activeAbortController?.abort();
|
|
255
|
-
}
|
|
256
|
-
catch { }
|
|
257
|
-
try {
|
|
258
|
-
managed.session.cancel();
|
|
259
|
-
}
|
|
260
|
-
catch { }
|
|
261
|
-
}
|
|
262
|
-
managed.lastActivity = Date.now();
|
|
263
|
-
const parts = [];
|
|
264
|
-
if (wasRunning)
|
|
265
|
-
parts.push('stopping current task');
|
|
266
|
-
if (queueSize > 0)
|
|
267
|
-
parts.push(`cleared ${queueSize} queued task${queueSize > 1 ? 's' : ''}`);
|
|
268
|
-
return { ok: true, message: `⏹ Cancelled: ${parts.join(', ')}.` };
|
|
269
|
-
}
|
|
270
210
|
async function processMessage(managed, msg) {
|
|
271
211
|
let turn = beginTurn(managed);
|
|
272
212
|
if (!turn)
|
|
@@ -970,626 +910,19 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
970
910
|
await sendUserVisible(msg, '⚠️ Too many active sessions. Please retry later.').catch(() => { });
|
|
971
911
|
return;
|
|
972
912
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
...(agentLine ? [agentLine] : []),
|
|
986
|
-
`Model: \`${managed.session.model}\``,
|
|
987
|
-
`Endpoint: \`${managed.config.endpoint || '?'}\``,
|
|
988
|
-
`Default dir: \`${managed.config.dir || defaultDir}\``,
|
|
989
|
-
'',
|
|
990
|
-
'Send me a coding task, or use /help for commands.',
|
|
991
|
-
];
|
|
992
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
if (content === '/help') {
|
|
996
|
-
const lines = [
|
|
997
|
-
'Commands:',
|
|
998
|
-
'/start — Welcome + config summary',
|
|
999
|
-
'/help — This message',
|
|
1000
|
-
'/version — Show version',
|
|
1001
|
-
'/new — Start a new session',
|
|
1002
|
-
'/cancel — Abort current generation',
|
|
1003
|
-
'/status — Session stats',
|
|
1004
|
-
'/watchdog [status] — Show watchdog settings/status',
|
|
1005
|
-
'/agent — Show current agent',
|
|
1006
|
-
'/agents — List all configured agents',
|
|
1007
|
-
'/escalate [model] — Use larger model for next message',
|
|
1008
|
-
'/deescalate — Return to base model',
|
|
1009
|
-
'/dir [path] — Get/set working directory',
|
|
1010
|
-
'/pin — Pin current working directory',
|
|
1011
|
-
'/model — Show current model',
|
|
1012
|
-
'/approval [mode] — Get/set approval mode',
|
|
1013
|
-
'/mode [code|sys] — Get/set mode',
|
|
1014
|
-
'/subagents [on|off] — Toggle sub-agents',
|
|
1015
|
-
'/compact — Trigger context compaction',
|
|
1016
|
-
'/changes — Show files modified this session',
|
|
1017
|
-
'/undo — Undo last edit',
|
|
1018
|
-
'/vault <query> — Search vault entries',
|
|
1019
|
-
'/anton <file> — Start autonomous task runner',
|
|
1020
|
-
'/anton status | /anton stop | /anton last',
|
|
1021
|
-
];
|
|
1022
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
if (content === '/model') {
|
|
1026
|
-
await sendUserVisible(msg, `Model: \`${managed.session.model}\`\nHarness: \`${managed.session.harness}\``).catch(() => { });
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
if (content === '/version') {
|
|
1030
|
-
await sendUserVisible(msg, `Idle Hands v${PKG_VERSION}`).catch(() => { });
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
if (content === '/compact') {
|
|
1034
|
-
managed.session.reset();
|
|
1035
|
-
await sendUserVisible(msg, '🗜 Session context compacted (reset to system prompt).').catch(() => { });
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
if (content === '/dir' || content.startsWith('/dir ')) {
|
|
1039
|
-
const arg = content.slice('/dir'.length).trim();
|
|
1040
|
-
if (!arg) {
|
|
1041
|
-
const lines = [
|
|
1042
|
-
`Working directory: \`${managed.config.dir || defaultDir}\``,
|
|
1043
|
-
`Directory pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
|
|
1044
|
-
];
|
|
1045
|
-
if (!managed.dirPinned && managed.repoCandidates.length > 1) {
|
|
1046
|
-
lines.push('Action required: run `/dir <repo-root>` before file edits.');
|
|
1047
|
-
lines.push(`Detected repos: ${managed.repoCandidates
|
|
1048
|
-
.slice(0, 5)
|
|
1049
|
-
.map((p) => `\`${p}\``)
|
|
1050
|
-
.join(', ')}`);
|
|
1051
|
-
}
|
|
1052
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
const resolvedDir = path.resolve(expandHome(arg));
|
|
1056
|
-
if (!isPathAllowed(resolvedDir, managed.allowedDirs)) {
|
|
1057
|
-
await sendUserVisible(msg, `❌ Directory not allowed. Allowed roots: ${managed.allowedDirs.map((d) => `\`${d}\``).join(', ')}`).catch(() => { });
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
|
|
1061
|
-
const cfg = {
|
|
1062
|
-
...managed.config,
|
|
1063
|
-
dir: resolvedDir,
|
|
1064
|
-
allowed_write_roots: managed.allowedDirs,
|
|
1065
|
-
dir_pinned: true,
|
|
1066
|
-
repo_candidates: repoCandidates,
|
|
1067
|
-
};
|
|
1068
|
-
await recreateSession(managed, cfg);
|
|
1069
|
-
managed.dirPinned = true;
|
|
1070
|
-
managed.repoCandidates = repoCandidates;
|
|
1071
|
-
await sendUserVisible(msg, `✅ Working directory pinned to \`${resolvedDir}\``).catch(() => { });
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
if (content === '/pin' || content.startsWith('/pin ')) {
|
|
1075
|
-
const arg = content.slice('/pin'.length).trim();
|
|
1076
|
-
const currentDir = managed.config.dir || defaultDir;
|
|
1077
|
-
if (!arg) {
|
|
1078
|
-
const resolvedDir = path.resolve(expandHome(currentDir));
|
|
1079
|
-
if (!isPathAllowed(resolvedDir, managed.allowedDirs)) {
|
|
1080
|
-
await sendUserVisible(msg, `❌ Directory not allowed. Allowed roots: ${managed.allowedDirs.map((d) => `\`${d}\``).join(', ')}`).catch(() => { });
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
const repoCandidates = await detectRepoCandidates(resolvedDir, managed.allowedDirs).catch(() => managed.repoCandidates);
|
|
1084
|
-
const cfg = {
|
|
1085
|
-
...managed.config,
|
|
1086
|
-
dir: resolvedDir,
|
|
1087
|
-
allowed_write_roots: managed.allowedDirs,
|
|
1088
|
-
dir_pinned: true,
|
|
1089
|
-
repo_candidates: repoCandidates,
|
|
1090
|
-
};
|
|
1091
|
-
await recreateSession(managed, cfg);
|
|
1092
|
-
managed.dirPinned = true;
|
|
1093
|
-
managed.repoCandidates = repoCandidates;
|
|
1094
|
-
await sendUserVisible(msg, `✅ Working directory pinned to \`${resolvedDir}\``).catch(() => { });
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (content === '/approval' || content.startsWith('/approval ')) {
|
|
1099
|
-
const arg = content.slice('/approval'.length).trim().toLowerCase();
|
|
1100
|
-
const modes = ['plan', 'default', 'auto-edit', 'yolo'];
|
|
1101
|
-
if (!arg) {
|
|
1102
|
-
await sendUserVisible(msg, `Approval mode: \`${managed.config.approval_mode || approvalMode}\`\nOptions: ${modes.join(', ')}`).catch(() => { });
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
if (!modes.includes(arg)) {
|
|
1106
|
-
await sendUserVisible(msg, `Invalid mode. Options: ${modes.join(', ')}`).catch(() => { });
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
managed.config.approval_mode = arg;
|
|
1110
|
-
managed.config.no_confirm = arg === 'yolo';
|
|
1111
|
-
await sendUserVisible(msg, `✅ Approval mode set to \`${arg}\``).catch(() => { });
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
if (content === '/mode' || content.startsWith('/mode ')) {
|
|
1115
|
-
const arg = content.slice('/mode'.length).trim().toLowerCase();
|
|
1116
|
-
if (!arg) {
|
|
1117
|
-
await sendUserVisible(msg, `Mode: \`${managed.config.mode || 'code'}\``).catch(() => { });
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
if (arg !== 'code' && arg !== 'sys') {
|
|
1121
|
-
await sendUserVisible(msg, 'Invalid mode. Options: code, sys').catch(() => { });
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
managed.config.mode = arg;
|
|
1125
|
-
if (arg === 'sys' && managed.config.approval_mode === 'auto-edit') {
|
|
1126
|
-
managed.config.approval_mode = 'default';
|
|
1127
|
-
}
|
|
1128
|
-
await sendUserVisible(msg, `✅ Mode set to \`${arg}\``).catch(() => { });
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (content === '/subagents' || content.startsWith('/subagents ')) {
|
|
1132
|
-
const arg = content.slice('/subagents'.length).trim().toLowerCase();
|
|
1133
|
-
const current = managed.config.sub_agents?.enabled !== false;
|
|
1134
|
-
if (!arg) {
|
|
1135
|
-
await sendUserVisible(msg, `Sub-agents: \`${current ? 'on' : 'off'}\`\nUsage: /subagents on | off`).catch(() => { });
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
if (arg !== 'on' && arg !== 'off') {
|
|
1139
|
-
await sendUserVisible(msg, 'Invalid value. Usage: /subagents on | off').catch(() => { });
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
const enabled = arg === 'on';
|
|
1143
|
-
managed.config.sub_agents = { ...(managed.config.sub_agents ?? {}), enabled };
|
|
1144
|
-
await sendUserVisible(msg, `✅ Sub-agents \`${enabled ? 'on' : 'off'}\`${!enabled ? ' — spawn_task disabled for this session' : ''}`).catch(() => { });
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (content === '/changes') {
|
|
1148
|
-
const replay = managed.session.replay;
|
|
1149
|
-
if (!replay) {
|
|
1150
|
-
await sendUserVisible(msg, 'Replay is disabled. No change tracking available.').catch(() => { });
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
try {
|
|
1154
|
-
const checkpoints = await replay.list(50);
|
|
1155
|
-
if (!checkpoints.length) {
|
|
1156
|
-
await sendUserVisible(msg, 'No file changes this session.').catch(() => { });
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
const byFile = new Map();
|
|
1160
|
-
for (const cp of checkpoints)
|
|
1161
|
-
byFile.set(cp.filePath, (byFile.get(cp.filePath) ?? 0) + 1);
|
|
1162
|
-
const lines = [`Session changes (${byFile.size} files):`];
|
|
1163
|
-
for (const [fp, count] of byFile)
|
|
1164
|
-
lines.push(`✎ \`${fp}\` (${count} edit${count > 1 ? 's' : ''})`);
|
|
1165
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1166
|
-
}
|
|
1167
|
-
catch (e) {
|
|
1168
|
-
await sendUserVisible(msg, `Error listing changes: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1169
|
-
}
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
if (content === '/undo') {
|
|
1173
|
-
const lastPath = managed.session.lastEditedPath;
|
|
1174
|
-
if (!lastPath) {
|
|
1175
|
-
await sendUserVisible(msg, 'No recent edits to undo.').catch(() => { });
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
try {
|
|
1179
|
-
const { undo_path } = await import('../tools.js');
|
|
1180
|
-
const result = await undo_path({ cwd: managed.config.dir || defaultDir, noConfirm: true, dryRun: false }, { path: lastPath });
|
|
1181
|
-
await sendUserVisible(msg, `✅ ${result}`).catch(() => { });
|
|
1182
|
-
}
|
|
1183
|
-
catch (e) {
|
|
1184
|
-
await sendUserVisible(msg, `❌ Undo failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1185
|
-
}
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
if (content === '/vault' || content.startsWith('/vault ')) {
|
|
1189
|
-
const query = content.slice('/vault'.length).trim();
|
|
1190
|
-
if (!query) {
|
|
1191
|
-
await sendUserVisible(msg, 'Usage: /vault <search query>').catch(() => { });
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const vault = managed.session.vault;
|
|
1195
|
-
if (!vault) {
|
|
1196
|
-
await sendUserVisible(msg, 'Vault is disabled.').catch(() => { });
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
try {
|
|
1200
|
-
const results = await vault.search(query, 5);
|
|
1201
|
-
if (!results.length) {
|
|
1202
|
-
await sendUserVisible(msg, `No vault results for "${query}"`).catch(() => { });
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
const lines = [`Vault results for "${query}":`];
|
|
1206
|
-
for (const r of results) {
|
|
1207
|
-
const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || '?'}`;
|
|
1208
|
-
const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 120);
|
|
1209
|
-
lines.push(`• ${title}: ${body}`);
|
|
1210
|
-
}
|
|
1211
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1212
|
-
}
|
|
1213
|
-
catch (e) {
|
|
1214
|
-
await sendUserVisible(msg, `Error searching vault: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1215
|
-
}
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
if (content === '/status') {
|
|
1219
|
-
const used = managed.session.currentContextTokens;
|
|
1220
|
-
const pct = managed.session.contextWindow > 0
|
|
1221
|
-
? Math.min(100, (used / managed.session.contextWindow) * 100).toFixed(1)
|
|
1222
|
-
: '?';
|
|
1223
|
-
const agentLine = managed.agentPersona
|
|
1224
|
-
? `Agent: ${managed.agentPersona.display_name || managed.agentId}`
|
|
1225
|
-
: null;
|
|
1226
|
-
await sendUserVisible(msg, [
|
|
1227
|
-
...(agentLine ? [agentLine] : []),
|
|
1228
|
-
`Mode: ${managed.config.mode ?? 'code'}`,
|
|
1229
|
-
`Approval: ${managed.config.approval_mode}`,
|
|
1230
|
-
`Model: ${managed.session.model}`,
|
|
1231
|
-
`Harness: ${managed.session.harness}`,
|
|
1232
|
-
`Dir: ${managed.config.dir ?? defaultDir}`,
|
|
1233
|
-
`Dir pinned: ${managed.dirPinned ? 'yes' : 'no'}`,
|
|
1234
|
-
`Context: ~${used}/${managed.session.contextWindow} (${pct}%)`,
|
|
1235
|
-
`State: ${managed.state}`,
|
|
1236
|
-
`Queue: ${managed.pendingQueue.length}/${maxQueue}`,
|
|
1237
|
-
].join('\n')).catch(() => { });
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
if (content === '/watchdog' || content === '/watchdog status') {
|
|
1241
|
-
await sendUserVisible(msg, watchdogStatusText(managed)).catch(() => { });
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
if (content.startsWith('/watchdog ')) {
|
|
1245
|
-
await sendUserVisible(msg, 'Usage: /watchdog or /watchdog status').catch(() => { });
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
// /agent - show current agent info
|
|
1249
|
-
if (content === '/agent') {
|
|
1250
|
-
if (!managed.agentPersona) {
|
|
1251
|
-
await sendUserVisible(msg, 'No agent configured. Using global config.').catch(() => { });
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const p = managed.agentPersona;
|
|
1255
|
-
const lines = [
|
|
1256
|
-
`**Agent: ${p.display_name || managed.agentId}** (\`${managed.agentId}\`)`,
|
|
1257
|
-
...(p.model ? [`Model: \`${p.model}\``] : []),
|
|
1258
|
-
...(p.endpoint ? [`Endpoint: \`${p.endpoint}\``] : []),
|
|
1259
|
-
...(p.approval_mode ? [`Approval: \`${p.approval_mode}\``] : []),
|
|
1260
|
-
...(p.default_dir ? [`Default dir: \`${p.default_dir}\``] : []),
|
|
1261
|
-
...(p.allowed_dirs?.length
|
|
1262
|
-
? [`Allowed dirs: ${p.allowed_dirs.map((d) => `\`${d}\``).join(', ')}`]
|
|
1263
|
-
: []),
|
|
1264
|
-
];
|
|
1265
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
// /agents - list all configured agents
|
|
1269
|
-
if (content === '/agents') {
|
|
1270
|
-
const agents = botConfig.agents;
|
|
1271
|
-
if (!agents || Object.keys(agents).length === 0) {
|
|
1272
|
-
await sendUserVisible(msg, 'No agents configured. Using global config.').catch(() => { });
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
const lines = ['**Configured Agents:**'];
|
|
1276
|
-
for (const [id, p] of Object.entries(agents)) {
|
|
1277
|
-
const current = id === managed.agentId ? ' ← current' : '';
|
|
1278
|
-
const model = p.model ? ` (${p.model})` : '';
|
|
1279
|
-
lines.push(`• **${p.display_name || id}** (\`${id}\`)${model}${current}`);
|
|
1280
|
-
}
|
|
1281
|
-
// Show routing rules
|
|
1282
|
-
const routing = botConfig.routing;
|
|
1283
|
-
if (routing) {
|
|
1284
|
-
lines.push('', '**Routing:**');
|
|
1285
|
-
if (routing.default)
|
|
1286
|
-
lines.push(`Default: \`${routing.default}\``);
|
|
1287
|
-
if (routing.users && Object.keys(routing.users).length > 0) {
|
|
1288
|
-
lines.push(`Users: ${Object.entries(routing.users)
|
|
1289
|
-
.map(([u, a]) => `${u}→${a}`)
|
|
1290
|
-
.join(', ')}`);
|
|
1291
|
-
}
|
|
1292
|
-
if (routing.channels && Object.keys(routing.channels).length > 0) {
|
|
1293
|
-
lines.push(`Channels: ${Object.entries(routing.channels)
|
|
1294
|
-
.map(([c, a]) => `${c}→${a}`)
|
|
1295
|
-
.join(', ')}`);
|
|
1296
|
-
}
|
|
1297
|
-
if (routing.guilds && Object.keys(routing.guilds).length > 0) {
|
|
1298
|
-
lines.push(`Guilds: ${Object.entries(routing.guilds)
|
|
1299
|
-
.map(([g, a]) => `${g}→${a}`)
|
|
1300
|
-
.join(', ')}`);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
// /escalate - explicitly escalate to a larger model for next message
|
|
1307
|
-
if (content === '/escalate' || content.startsWith('/escalate ')) {
|
|
1308
|
-
const escalation = managed.agentPersona?.escalation;
|
|
1309
|
-
if (!escalation || !escalation.models?.length) {
|
|
1310
|
-
await sendUserVisible(msg, '❌ No escalation models configured for this agent.').catch(() => { });
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
const arg = content.slice('/escalate'.length).trim();
|
|
1314
|
-
// No arg: show available models and current state
|
|
1315
|
-
if (!arg) {
|
|
1316
|
-
const currentModel = managed.config.model || config.model || 'default';
|
|
1317
|
-
const lines = [
|
|
1318
|
-
`**Current model:** \`${currentModel}\``,
|
|
1319
|
-
`**Escalation models:** ${escalation.models.map((m) => `\`${m}\``).join(', ')}`,
|
|
1320
|
-
'',
|
|
1321
|
-
'Usage: `/escalate <model>` or `/escalate next`',
|
|
1322
|
-
'Then send your message - it will use the escalated model.',
|
|
1323
|
-
];
|
|
1324
|
-
if (managed.pendingEscalation) {
|
|
1325
|
-
lines.push('', `⚡ **Pending escalation:** \`${managed.pendingEscalation}\` (next message will use this)`);
|
|
1326
|
-
}
|
|
1327
|
-
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
// Handle 'next' - escalate to next model in chain
|
|
1331
|
-
let targetModel;
|
|
1332
|
-
if (arg.toLowerCase() === 'next') {
|
|
1333
|
-
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
1334
|
-
targetModel = escalation.models[nextIndex];
|
|
1335
|
-
}
|
|
1336
|
-
else {
|
|
1337
|
-
// Specific model requested
|
|
1338
|
-
if (!escalation.models.includes(arg)) {
|
|
1339
|
-
await sendUserVisible(msg, `❌ Model \`${arg}\` not in escalation chain. Available: ${escalation.models.map((m) => `\`${m}\``).join(', ')}`).catch(() => { });
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
targetModel = arg;
|
|
1343
|
-
}
|
|
1344
|
-
managed.pendingEscalation = targetModel;
|
|
1345
|
-
await sendUserVisible(msg, `⚡ Next message will use \`${targetModel}\`. Send your request now.`).catch(() => { });
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
// /deescalate - return to base model
|
|
1349
|
-
if (content === '/deescalate') {
|
|
1350
|
-
if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
|
|
1351
|
-
await sendUserVisible(msg, 'Already using base model.').catch(() => { });
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
const baseModel = managed.agentPersona?.model || config.model || 'default';
|
|
1355
|
-
managed.pendingEscalation = null;
|
|
1356
|
-
managed.currentModelIndex = 0;
|
|
1357
|
-
// Recreate session with base model
|
|
1358
|
-
const cfg = {
|
|
1359
|
-
...managed.config,
|
|
1360
|
-
model: baseModel,
|
|
1361
|
-
};
|
|
1362
|
-
await recreateSession(managed, cfg);
|
|
1363
|
-
await sendUserVisible(msg, `✅ Returned to base model: \`${baseModel}\``).catch(() => { });
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
// /git_status - show git status for working directory
|
|
1367
|
-
if (content === '/git_status') {
|
|
1368
|
-
const cwd = managed.config.dir || defaultDir;
|
|
1369
|
-
if (!cwd) {
|
|
1370
|
-
await sendUserVisible(msg, 'No working directory set. Use `/dir` to set one.').catch(() => { });
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
try {
|
|
1374
|
-
const { spawnSync } = await import('node:child_process');
|
|
1375
|
-
// Run git status -s
|
|
1376
|
-
const statusResult = spawnSync('git', ['status', '-s'], {
|
|
1377
|
-
cwd,
|
|
1378
|
-
encoding: 'utf8',
|
|
1379
|
-
timeout: 5000,
|
|
1380
|
-
});
|
|
1381
|
-
if (statusResult.status !== 0) {
|
|
1382
|
-
const err = String(statusResult.stderr || statusResult.error || 'Unknown error');
|
|
1383
|
-
if (err.includes('not a git repository') || err.includes('not in a git')) {
|
|
1384
|
-
await sendUserVisible(msg, '❌ Not a git repository.').catch(() => { });
|
|
1385
|
-
}
|
|
1386
|
-
else {
|
|
1387
|
-
await sendUserVisible(msg, `❌ git status failed: ${err.slice(0, 200)}`).catch(() => { });
|
|
1388
|
-
}
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
const statusOut = String(statusResult.stdout || '').trim();
|
|
1392
|
-
// Get branch info
|
|
1393
|
-
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1394
|
-
cwd,
|
|
1395
|
-
encoding: 'utf8',
|
|
1396
|
-
timeout: 2000,
|
|
1397
|
-
});
|
|
1398
|
-
const branch = branchResult.status === 0 ? String(branchResult.stdout || '').trim() : 'unknown';
|
|
1399
|
-
if (!statusOut) {
|
|
1400
|
-
await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n✅ Working tree clean`).catch(() => { });
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
const lines = statusOut.split('\n').slice(0, 30);
|
|
1404
|
-
const truncated = statusOut.split('\n').length > 30;
|
|
1405
|
-
const formatted = lines
|
|
1406
|
-
.map((line) => `\`${line.slice(0, 2)}\` ${line.slice(3)}`)
|
|
1407
|
-
.join('\n');
|
|
1408
|
-
await sendUserVisible(msg, `📁 \`${cwd}\`\n🌿 Branch: \`${branch}\`\n\n\`\`\`\n${formatted}${truncated ? '\n...' : ''}\`\`\``).catch(() => { });
|
|
1409
|
-
}
|
|
1410
|
-
catch (e) {
|
|
1411
|
-
await sendUserVisible(msg, `❌ git status failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1412
|
-
}
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
if (content === '/hosts') {
|
|
1416
|
-
try {
|
|
1417
|
-
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
1418
|
-
const config = await loadRuntimes();
|
|
1419
|
-
const redacted = redactConfig(config);
|
|
1420
|
-
if (!redacted.hosts.length) {
|
|
1421
|
-
await sendUserVisible(msg, 'No hosts configured. Use `idlehands hosts add` in CLI.').catch(() => { });
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
const lines = redacted.hosts.map((h) => `${h.enabled ? '🟢' : '🔴'} ${h.display_name} (\`${h.id}\`)\n Transport: ${h.transport}`);
|
|
1425
|
-
const chunks = splitDiscord(lines.join('\n\n'));
|
|
1426
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1427
|
-
if (i === 0)
|
|
1428
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1429
|
-
else
|
|
1430
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
catch (e) {
|
|
1434
|
-
await sendUserVisible(msg, `❌ Failed to load hosts: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1435
|
-
}
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
if (content === '/backends') {
|
|
1439
|
-
try {
|
|
1440
|
-
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
1441
|
-
const config = await loadRuntimes();
|
|
1442
|
-
const redacted = redactConfig(config);
|
|
1443
|
-
if (!redacted.backends.length) {
|
|
1444
|
-
await sendUserVisible(msg, 'No backends configured. Use `idlehands backends add` in CLI.').catch(() => { });
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
const lines = redacted.backends.map((b) => `${b.enabled ? '🟢' : '🔴'} ${b.display_name} (\`${b.id}\`)\n Type: ${b.type}`);
|
|
1448
|
-
const chunks = splitDiscord(lines.join('\n\n'));
|
|
1449
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1450
|
-
if (i === 0)
|
|
1451
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1452
|
-
else
|
|
1453
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
catch (e) {
|
|
1457
|
-
await sendUserVisible(msg, `❌ Failed to load backends: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1458
|
-
}
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
if (content === '/models' || content === '/rtmodels') {
|
|
1462
|
-
try {
|
|
1463
|
-
const { loadRuntimes } = await import('../runtime/store.js');
|
|
1464
|
-
const config = await loadRuntimes();
|
|
1465
|
-
if (!config.models.length) {
|
|
1466
|
-
await sendUserVisible(msg, 'No runtime models configured.').catch(() => { });
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
const enabledModels = config.models.filter((m) => m.enabled);
|
|
1470
|
-
if (!enabledModels.length) {
|
|
1471
|
-
await sendUserVisible(msg, 'No enabled runtime models. Use `idlehands models enable <id>` in CLI.').catch(() => { });
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
// Create buttons for model selection (Discord max 5 buttons per row, 5 rows)
|
|
1475
|
-
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import('discord.js');
|
|
1476
|
-
const rows = [];
|
|
1477
|
-
let currentRow = new ActionRowBuilder();
|
|
1478
|
-
for (const m of enabledModels) {
|
|
1479
|
-
const btn = new ButtonBuilder()
|
|
1480
|
-
.setCustomId(`model_switch:${m.id}`)
|
|
1481
|
-
.setLabel(m.display_name.slice(0, 80))
|
|
1482
|
-
.setStyle(ButtonStyle.Primary);
|
|
1483
|
-
currentRow.addComponents(btn);
|
|
1484
|
-
if (currentRow.components.length >= 5) {
|
|
1485
|
-
rows.push(currentRow);
|
|
1486
|
-
currentRow = new ActionRowBuilder();
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
if (currentRow.components.length > 0) {
|
|
1490
|
-
rows.push(currentRow);
|
|
1491
|
-
}
|
|
1492
|
-
await msg.channel.send({
|
|
1493
|
-
content: '📋 **Select a model to switch to:**',
|
|
1494
|
-
components: rows.slice(0, 5), // Discord max 5 rows
|
|
1495
|
-
}).catch(() => { });
|
|
1496
|
-
}
|
|
1497
|
-
catch (e) {
|
|
1498
|
-
await sendUserVisible(msg, `❌ Failed to load runtime models: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1499
|
-
}
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
if (content === '/rtstatus') {
|
|
1503
|
-
try {
|
|
1504
|
-
const { loadActiveRuntime } = await import('../runtime/executor.js');
|
|
1505
|
-
const active = await loadActiveRuntime();
|
|
1506
|
-
if (!active) {
|
|
1507
|
-
await sendUserVisible(msg, 'No active runtime.').catch(() => { });
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
const lines = [
|
|
1511
|
-
'Active Runtime',
|
|
1512
|
-
`Model: \`${active.modelId}\``,
|
|
1513
|
-
`Backend: \`${active.backendId ?? 'none'}\``,
|
|
1514
|
-
`Hosts: ${active.hostIds.map((id) => `\`${id}\``).join(', ') || 'none'}`,
|
|
1515
|
-
`Healthy: ${active.healthy ? '✅ yes' : '❌ no'}`,
|
|
1516
|
-
`Endpoint: \`${active.endpoint ?? 'unknown'}\``,
|
|
1517
|
-
`Started: \`${active.startedAt}\``,
|
|
1518
|
-
];
|
|
1519
|
-
const chunks = splitDiscord(lines.join('\n'));
|
|
1520
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
1521
|
-
if (i === 0)
|
|
1522
|
-
await sendUserVisible(msg, chunk).catch(() => { });
|
|
1523
|
-
else
|
|
1524
|
-
await msg.channel.send(chunk).catch(() => { });
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
catch (e) {
|
|
1528
|
-
await sendUserVisible(msg, `❌ Failed to read runtime status: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1529
|
-
}
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
if (content === '/switch' || content.startsWith('/switch ')) {
|
|
1533
|
-
try {
|
|
1534
|
-
const modelId = content.slice('/switch'.length).trim();
|
|
1535
|
-
if (!modelId) {
|
|
1536
|
-
await sendUserVisible(msg, 'Usage: /switch <model-id>').catch(() => { });
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
const { plan } = await import('../runtime/planner.js');
|
|
1540
|
-
const { execute, loadActiveRuntime } = await import('../runtime/executor.js');
|
|
1541
|
-
const { loadRuntimes } = await import('../runtime/store.js');
|
|
1542
|
-
const rtConfig = await loadRuntimes();
|
|
1543
|
-
const active = await loadActiveRuntime();
|
|
1544
|
-
const result = plan({ modelId, mode: 'live' }, rtConfig, active);
|
|
1545
|
-
if (!result.ok) {
|
|
1546
|
-
await sendUserVisible(msg, `❌ Plan failed: ${result.reason}`).catch(() => { });
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
if (result.reuse) {
|
|
1550
|
-
await sendUserVisible(msg, '✅ Runtime already active and healthy.').catch(() => { });
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
const statusMsg = await sendUserVisible(msg, `⏳ Switching to \`${result.model.display_name}\`...`).catch(() => null);
|
|
1554
|
-
const execResult = await execute(result, {
|
|
1555
|
-
onStep: async (step, status) => {
|
|
1556
|
-
if (status === 'done' && statusMsg) {
|
|
1557
|
-
await statusMsg.edit(`⏳ ${step.description}... ✓`).catch(() => { });
|
|
1558
|
-
}
|
|
1559
|
-
},
|
|
1560
|
-
confirm: async (prompt) => {
|
|
1561
|
-
await sendUserVisible(msg, `⚠️ ${prompt}\nAuto-approving for bot context.`).catch(() => { });
|
|
1562
|
-
return true;
|
|
1563
|
-
},
|
|
1564
|
-
});
|
|
1565
|
-
if (execResult.ok) {
|
|
1566
|
-
if (statusMsg) {
|
|
1567
|
-
await statusMsg.edit(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
1568
|
-
}
|
|
1569
|
-
else {
|
|
1570
|
-
await sendUserVisible(msg, `✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
else {
|
|
1574
|
-
const err = `❌ Switch failed: ${execResult.error || 'unknown error'}`;
|
|
1575
|
-
if (statusMsg) {
|
|
1576
|
-
await statusMsg.edit(err).catch(() => { });
|
|
1577
|
-
}
|
|
1578
|
-
else {
|
|
1579
|
-
await sendUserVisible(msg, err).catch(() => { });
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
catch (e) {
|
|
1584
|
-
await sendUserVisible(msg, `❌ Switch failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
1585
|
-
}
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
// /anton command
|
|
1589
|
-
if (content === '/anton' || content.startsWith('/anton ')) {
|
|
1590
|
-
await handleDiscordAnton(managed, msg, content);
|
|
913
|
+
const cmdCtx = {
|
|
914
|
+
sendUserVisible,
|
|
915
|
+
cancelActive,
|
|
916
|
+
recreateSession,
|
|
917
|
+
watchdogStatusText,
|
|
918
|
+
defaultDir,
|
|
919
|
+
config,
|
|
920
|
+
botConfig,
|
|
921
|
+
approvalMode,
|
|
922
|
+
maxQueue,
|
|
923
|
+
};
|
|
924
|
+
if (await handleTextCommand(managed, msg, content, cmdCtx))
|
|
1591
925
|
return;
|
|
1592
|
-
}
|
|
1593
926
|
if (managed.inFlight) {
|
|
1594
927
|
if (managed.pendingQueue.length >= maxQueue) {
|
|
1595
928
|
await sendUserVisible(msg, `⏳ Queue full (${managed.pendingQueue.length}/${maxQueue}). Use /cancel.`).catch(() => { });
|
|
@@ -1609,193 +942,6 @@ When you escalate, your request will be re-run on a more capable model.`;
|
|
|
1609
942
|
await sendUserVisible(msg, `⚠️ Bot error: ${errMsg.length > 300 ? errMsg.slice(0, 297) + '...' : errMsg}`).catch(() => { });
|
|
1610
943
|
});
|
|
1611
944
|
});
|
|
1612
|
-
const DISCORD_RATE_LIMIT_MS = 15_000;
|
|
1613
|
-
async function handleDiscordAnton(managed, msg, content) {
|
|
1614
|
-
const args = content.replace(/^\/anton\s*/, '').trim();
|
|
1615
|
-
const sub = firstToken(args);
|
|
1616
|
-
if (!sub || sub === 'status') {
|
|
1617
|
-
if (!managed.antonActive) {
|
|
1618
|
-
await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
|
|
1619
|
-
}
|
|
1620
|
-
else if (managed.antonAbortSignal?.aborted) {
|
|
1621
|
-
await sendUserVisible(msg, '🛑 Anton is stopping. Please wait for the current attempt to unwind.').catch(() => { });
|
|
1622
|
-
}
|
|
1623
|
-
else if (managed.antonProgress) {
|
|
1624
|
-
const line1 = formatProgressBar(managed.antonProgress);
|
|
1625
|
-
if (managed.antonProgress.currentTask) {
|
|
1626
|
-
await sendUserVisible(msg, `${line1}\n\n**Working on:** *${managed.antonProgress.currentTask}* (Attempt ${managed.antonProgress.currentAttempt})`).catch(() => { });
|
|
1627
|
-
}
|
|
1628
|
-
else {
|
|
1629
|
-
await sendUserVisible(msg, line1).catch(() => { });
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
else {
|
|
1633
|
-
await sendUserVisible(msg, '🤖 Anton is running (no progress data yet).').catch(() => { });
|
|
1634
|
-
}
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
if (sub === 'stop') {
|
|
1638
|
-
if (!managed.antonActive || !managed.antonAbortSignal) {
|
|
1639
|
-
await sendUserVisible(msg, 'No Anton run in progress.').catch(() => { });
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
managed.lastActivity = Date.now();
|
|
1643
|
-
managed.antonAbortSignal.aborted = true;
|
|
1644
|
-
await sendUserVisible(msg, '🛑 Anton stop requested.').catch(() => { });
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (sub === 'last') {
|
|
1648
|
-
if (!managed.antonLastResult) {
|
|
1649
|
-
await sendUserVisible(msg, 'No previous Anton run.').catch(() => { });
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
await sendUserVisible(msg, formatRunSummary(managed.antonLastResult)).catch(() => { });
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
const filePart = sub === 'run' ? args.replace(/^\S+\s*/, '').trim() : args;
|
|
1656
|
-
if (!filePart) {
|
|
1657
|
-
await sendUserVisible(msg, '/anton <file> — start | /anton status | /anton stop | /anton last').catch(() => { });
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
if (managed.antonActive) {
|
|
1661
|
-
const staleMs = Date.now() - managed.lastActivity;
|
|
1662
|
-
if (staleMs > 120_000) {
|
|
1663
|
-
managed.antonActive = false;
|
|
1664
|
-
managed.antonAbortSignal = null;
|
|
1665
|
-
managed.antonProgress = null;
|
|
1666
|
-
await sendUserVisible(msg, '♻️ Recovered stale Anton run state. Starting a fresh run...').catch(() => { });
|
|
1667
|
-
}
|
|
1668
|
-
else {
|
|
1669
|
-
const runningMsg = managed.antonAbortSignal?.aborted
|
|
1670
|
-
? '🛑 Anton is still stopping. Please wait a moment, then try again.'
|
|
1671
|
-
: '⚠️ Anton is already running. Use /anton stop first.';
|
|
1672
|
-
await sendUserVisible(msg, runningMsg).catch(() => { });
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
const cwd = managed.config.dir || process.cwd();
|
|
1677
|
-
const filePath = path.resolve(cwd, filePart);
|
|
1678
|
-
try {
|
|
1679
|
-
await fs.stat(filePath);
|
|
1680
|
-
}
|
|
1681
|
-
catch {
|
|
1682
|
-
await sendUserVisible(msg, `File not found: ${filePath}`).catch(() => { });
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
const defaults = managed.config.anton || {};
|
|
1686
|
-
const runConfig = {
|
|
1687
|
-
taskFile: filePath,
|
|
1688
|
-
projectDir: defaults.project_dir || cwd,
|
|
1689
|
-
maxRetriesPerTask: defaults.max_retries ?? 3,
|
|
1690
|
-
maxIterations: defaults.max_iterations ?? 200,
|
|
1691
|
-
taskMaxIterations: defaults.task_max_iterations ?? 50,
|
|
1692
|
-
taskTimeoutSec: defaults.task_timeout_sec ?? 600,
|
|
1693
|
-
totalTimeoutSec: defaults.total_timeout_sec ?? 7200,
|
|
1694
|
-
maxTotalTokens: defaults.max_total_tokens ?? Infinity,
|
|
1695
|
-
maxPromptTokensPerAttempt: defaults.max_prompt_tokens_per_attempt ?? 128_000,
|
|
1696
|
-
autoCommit: defaults.auto_commit ?? true,
|
|
1697
|
-
branch: false,
|
|
1698
|
-
allowDirty: false,
|
|
1699
|
-
aggressiveCleanOnFail: false,
|
|
1700
|
-
verifyAi: defaults.verify_ai ?? true,
|
|
1701
|
-
verifyModel: undefined,
|
|
1702
|
-
decompose: defaults.decompose ?? true,
|
|
1703
|
-
maxDecomposeDepth: defaults.max_decompose_depth ?? 2,
|
|
1704
|
-
maxTotalTasks: defaults.max_total_tasks ?? 500,
|
|
1705
|
-
buildCommand: defaults.build_command ?? undefined,
|
|
1706
|
-
testCommand: defaults.test_command ?? undefined,
|
|
1707
|
-
lintCommand: defaults.lint_command ?? undefined,
|
|
1708
|
-
skipOnFail: defaults.skip_on_fail ?? false,
|
|
1709
|
-
skipOnBlocked: defaults.skip_on_blocked ?? true,
|
|
1710
|
-
rollbackOnFail: defaults.rollback_on_fail ?? false,
|
|
1711
|
-
maxIdenticalFailures: defaults.max_identical_failures ?? 5,
|
|
1712
|
-
approvalMode: (defaults.approval_mode ?? 'yolo'),
|
|
1713
|
-
verbose: false,
|
|
1714
|
-
dryRun: false,
|
|
1715
|
-
};
|
|
1716
|
-
const abortSignal = { aborted: false };
|
|
1717
|
-
managed.antonActive = true;
|
|
1718
|
-
managed.antonAbortSignal = abortSignal;
|
|
1719
|
-
managed.antonProgress = null;
|
|
1720
|
-
let lastProgressAt = 0;
|
|
1721
|
-
const channel = msg.channel;
|
|
1722
|
-
const progress = {
|
|
1723
|
-
onTaskStart(task, attempt, prog) {
|
|
1724
|
-
managed.antonProgress = prog;
|
|
1725
|
-
managed.lastActivity = Date.now();
|
|
1726
|
-
const now = Date.now();
|
|
1727
|
-
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
1728
|
-
lastProgressAt = now;
|
|
1729
|
-
channel.send(formatTaskStart(task, attempt, prog)).catch(() => { });
|
|
1730
|
-
}
|
|
1731
|
-
},
|
|
1732
|
-
onTaskEnd(task, result, prog) {
|
|
1733
|
-
managed.antonProgress = prog;
|
|
1734
|
-
managed.lastActivity = Date.now();
|
|
1735
|
-
const now = Date.now();
|
|
1736
|
-
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
1737
|
-
lastProgressAt = now;
|
|
1738
|
-
channel.send(formatTaskEnd(task, result, prog)).catch(() => { });
|
|
1739
|
-
}
|
|
1740
|
-
},
|
|
1741
|
-
onTaskSkip(task, reason) {
|
|
1742
|
-
managed.lastActivity = Date.now();
|
|
1743
|
-
channel.send(formatTaskSkip(task, reason)).catch(() => { });
|
|
1744
|
-
},
|
|
1745
|
-
onRunComplete(result) {
|
|
1746
|
-
managed.lastActivity = Date.now();
|
|
1747
|
-
managed.antonLastResult = result;
|
|
1748
|
-
managed.antonActive = false;
|
|
1749
|
-
managed.antonAbortSignal = null;
|
|
1750
|
-
managed.antonProgress = null;
|
|
1751
|
-
channel.send(formatRunSummary(result)).catch(() => { });
|
|
1752
|
-
},
|
|
1753
|
-
onHeartbeat() {
|
|
1754
|
-
managed.lastActivity = Date.now();
|
|
1755
|
-
},
|
|
1756
|
-
onToolLoop(taskText, event) {
|
|
1757
|
-
managed.lastActivity = Date.now();
|
|
1758
|
-
if (defaults.progress_events !== false) {
|
|
1759
|
-
channel.send(formatToolLoopEvent(taskText, event)).catch(() => { });
|
|
1760
|
-
}
|
|
1761
|
-
},
|
|
1762
|
-
onCompaction(taskText, event) {
|
|
1763
|
-
managed.lastActivity = Date.now();
|
|
1764
|
-
// Only send for significant compactions to avoid noise
|
|
1765
|
-
if (defaults.progress_events !== false && event.droppedMessages >= 5) {
|
|
1766
|
-
channel.send(formatCompactionEvent(taskText, event)).catch(() => { });
|
|
1767
|
-
}
|
|
1768
|
-
},
|
|
1769
|
-
onVerification(taskText, verification) {
|
|
1770
|
-
managed.lastActivity = Date.now();
|
|
1771
|
-
// Only send for failures — successes are already reported in onTaskEnd
|
|
1772
|
-
if (defaults.progress_events !== false && !verification.passed) {
|
|
1773
|
-
channel.send(formatVerificationDetail(taskText, verification)).catch(() => { });
|
|
1774
|
-
}
|
|
1775
|
-
},
|
|
1776
|
-
};
|
|
1777
|
-
let pendingCount = 0;
|
|
1778
|
-
try {
|
|
1779
|
-
const tf = await parseTaskFile(filePath);
|
|
1780
|
-
pendingCount = tf.pending.length;
|
|
1781
|
-
}
|
|
1782
|
-
catch { }
|
|
1783
|
-
await sendUserVisible(msg, `🤖 Anton started on ${filePart} (${pendingCount} tasks pending)`).catch(() => { });
|
|
1784
|
-
runAnton({
|
|
1785
|
-
config: runConfig,
|
|
1786
|
-
idlehandsConfig: managed.config,
|
|
1787
|
-
progress,
|
|
1788
|
-
abortSignal,
|
|
1789
|
-
vault: managed.session.vault,
|
|
1790
|
-
lens: managed.session.lens,
|
|
1791
|
-
}).catch((err) => {
|
|
1792
|
-
managed.lastActivity = Date.now();
|
|
1793
|
-
managed.antonActive = false;
|
|
1794
|
-
managed.antonAbortSignal = null;
|
|
1795
|
-
managed.antonProgress = null;
|
|
1796
|
-
channel.send(`Anton error: ${err.message}`).catch(() => { });
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
945
|
const shutdown = async () => {
|
|
1800
946
|
clearInterval(cleanupTimer);
|
|
1801
947
|
for (const key of sessions.keys())
|