@visorcraft/idlehands 0.9.1
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/agent.js +2604 -0
- package/dist/agent.js.map +1 -0
- package/dist/anton/controller.js +341 -0
- package/dist/anton/controller.js.map +1 -0
- package/dist/anton/lock.js +110 -0
- package/dist/anton/lock.js.map +1 -0
- package/dist/anton/parser.js +303 -0
- package/dist/anton/parser.js.map +1 -0
- package/dist/anton/prompt.js +203 -0
- package/dist/anton/prompt.js.map +1 -0
- package/dist/anton/reporter.js +119 -0
- package/dist/anton/reporter.js.map +1 -0
- package/dist/anton/session.js +51 -0
- package/dist/anton/session.js.map +1 -0
- package/dist/anton/types.js +7 -0
- package/dist/anton/types.js.map +1 -0
- package/dist/anton/verifier.js +263 -0
- package/dist/anton/verifier.js.map +1 -0
- package/dist/bench/compare.js +239 -0
- package/dist/bench/compare.js.map +1 -0
- package/dist/bench/debug_hooks.js +17 -0
- package/dist/bench/debug_hooks.js.map +1 -0
- package/dist/bench/json_extract.js +22 -0
- package/dist/bench/json_extract.js.map +1 -0
- package/dist/bench/openclaw.js +86 -0
- package/dist/bench/openclaw.js.map +1 -0
- package/dist/bench/report.js +116 -0
- package/dist/bench/report.js.map +1 -0
- package/dist/bench/runner.js +312 -0
- package/dist/bench/runner.js.map +1 -0
- package/dist/bench/types.js +2 -0
- package/dist/bench/types.js.map +1 -0
- package/dist/bot/commands.js +444 -0
- package/dist/bot/commands.js.map +1 -0
- package/dist/bot/confirm-discord.js +133 -0
- package/dist/bot/confirm-discord.js.map +1 -0
- package/dist/bot/confirm-telegram.js +290 -0
- package/dist/bot/confirm-telegram.js.map +1 -0
- package/dist/bot/discord.js +826 -0
- package/dist/bot/discord.js.map +1 -0
- package/dist/bot/format.js +210 -0
- package/dist/bot/format.js.map +1 -0
- package/dist/bot/session-manager.js +270 -0
- package/dist/bot/session-manager.js.map +1 -0
- package/dist/bot/telegram.js +678 -0
- package/dist/bot/telegram.js.map +1 -0
- package/dist/cli/agent-turn.js +45 -0
- package/dist/cli/agent-turn.js.map +1 -0
- package/dist/cli/args.js +236 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bot.js +252 -0
- package/dist/cli/bot.js.map +1 -0
- package/dist/cli/build-repl-context.js +365 -0
- package/dist/cli/build-repl-context.js.map +1 -0
- package/dist/cli/command-registry.js +20 -0
- package/dist/cli/command-registry.js.map +1 -0
- package/dist/cli/commands/anton.js +271 -0
- package/dist/cli/commands/anton.js.map +1 -0
- package/dist/cli/commands/editing.js +328 -0
- package/dist/cli/commands/editing.js.map +1 -0
- package/dist/cli/commands/model.js +274 -0
- package/dist/cli/commands/model.js.map +1 -0
- package/dist/cli/commands/project.js +255 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/commands/runtime.js +63 -0
- package/dist/cli/commands/runtime.js.map +1 -0
- package/dist/cli/commands/session.js +281 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/tools.js +126 -0
- package/dist/cli/commands/tools.js.map +1 -0
- package/dist/cli/commands/trifecta.js +221 -0
- package/dist/cli/commands/trifecta.js.map +1 -0
- package/dist/cli/commands/tui.js +17 -0
- package/dist/cli/commands/tui.js.map +1 -0
- package/dist/cli/init.js +222 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/input.js +360 -0
- package/dist/cli/input.js.map +1 -0
- package/dist/cli/oneshot.js +254 -0
- package/dist/cli/oneshot.js.map +1 -0
- package/dist/cli/repl-context.js +2 -0
- package/dist/cli/repl-context.js.map +1 -0
- package/dist/cli/runtime-cmds.js +811 -0
- package/dist/cli/runtime-cmds.js.map +1 -0
- package/dist/cli/service.js +145 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/session-state.js +130 -0
- package/dist/cli/session-state.js.map +1 -0
- package/dist/cli/setup.js +815 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/shell.js +79 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/status.js +392 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/watch.js +33 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/client.js +676 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.js +194 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.js +507 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm/auto.js +13 -0
- package/dist/confirm/auto.js.map +1 -0
- package/dist/confirm/headless.js +41 -0
- package/dist/confirm/headless.js.map +1 -0
- package/dist/confirm/terminal.js +90 -0
- package/dist/confirm/terminal.js.map +1 -0
- package/dist/context.js +49 -0
- package/dist/context.js.map +1 -0
- package/dist/git.js +136 -0
- package/dist/git.js.map +1 -0
- package/dist/harnesses.js +171 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/history.js +139 -0
- package/dist/history.js.map +1 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.js +374 -0
- package/dist/indexer.js.map +1 -0
- package/dist/jsonrpc.js +76 -0
- package/dist/jsonrpc.js.map +1 -0
- package/dist/lens.js +525 -0
- package/dist/lens.js.map +1 -0
- package/dist/lsp.js +605 -0
- package/dist/lsp.js.map +1 -0
- package/dist/markdown.js +275 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.js +554 -0
- package/dist/mcp.js.map +1 -0
- package/dist/recovery.js +178 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.js +132 -0
- package/dist/replay.js.map +1 -0
- package/dist/replay_cli.js +24 -0
- package/dist/replay_cli.js.map +1 -0
- package/dist/runtime/executor.js +418 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/planner.js +197 -0
- package/dist/runtime/planner.js.map +1 -0
- package/dist/runtime/store.js +289 -0
- package/dist/runtime/store.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/safety.js +446 -0
- package/dist/safety.js.map +1 -0
- package/dist/spinner.js +224 -0
- package/dist/spinner.js.map +1 -0
- package/dist/sys/context.js +124 -0
- package/dist/sys/context.js.map +1 -0
- package/dist/sys/snapshot.sh +97 -0
- package/dist/term.js +61 -0
- package/dist/term.js.map +1 -0
- package/dist/themes.js +135 -0
- package/dist/themes.js.map +1 -0
- package/dist/tools.js +1114 -0
- package/dist/tools.js.map +1 -0
- package/dist/tui/branch-picker.js +65 -0
- package/dist/tui/branch-picker.js.map +1 -0
- package/dist/tui/command-handler.js +108 -0
- package/dist/tui/command-handler.js.map +1 -0
- package/dist/tui/confirm.js +90 -0
- package/dist/tui/confirm.js.map +1 -0
- package/dist/tui/controller.js +463 -0
- package/dist/tui/controller.js.map +1 -0
- package/dist/tui/event-bridge.js +44 -0
- package/dist/tui/event-bridge.js.map +1 -0
- package/dist/tui/events.js +2 -0
- package/dist/tui/events.js.map +1 -0
- package/dist/tui/keymap.js +144 -0
- package/dist/tui/keymap.js.map +1 -0
- package/dist/tui/layout.js +11 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/render.js +186 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/screen.js +48 -0
- package/dist/tui/screen.js.map +1 -0
- package/dist/tui/state.js +167 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/theme.js +70 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upgrade.js +412 -0
- package/dist/upgrade.js.map +1 -0
- package/dist/utils.js +87 -0
- package/dist/utils.js.map +1 -0
- package/dist/vault.js +520 -0
- package/dist/vault.js.map +1 -0
- package/dist/vim.js +160 -0
- package/dist/vim.js.map +1 -0
- package/package.json +67 -0
- package/src/sys/snapshot.sh +97 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import { Client, Events, GatewayIntentBits, Partials, } from 'discord.js';
|
|
2
|
+
import { createSession } from '../agent.js';
|
|
3
|
+
import { DiscordConfirmProvider } from './confirm-discord.js';
|
|
4
|
+
import { projectDir } from '../utils.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { runAnton } from '../anton/controller.js';
|
|
8
|
+
import { parseTaskFile } from '../anton/parser.js';
|
|
9
|
+
import { formatRunSummary, formatProgressBar, formatTaskStart, formatTaskEnd, formatTaskSkip } from '../anton/reporter.js';
|
|
10
|
+
function parseAllowedUsers(cfg) {
|
|
11
|
+
const fromEnv = process.env.IDLEHANDS_DISCORD_ALLOWED_USERS;
|
|
12
|
+
if (fromEnv && fromEnv.trim()) {
|
|
13
|
+
return new Set(fromEnv
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((s) => s.trim())
|
|
16
|
+
.filter(Boolean));
|
|
17
|
+
}
|
|
18
|
+
const values = Array.isArray(cfg.allowed_users) ? cfg.allowed_users : [];
|
|
19
|
+
return new Set(values.map((v) => String(v).trim()).filter(Boolean));
|
|
20
|
+
}
|
|
21
|
+
function normalizeApprovalMode(mode, fallback) {
|
|
22
|
+
const m = String(mode ?? '').trim().toLowerCase();
|
|
23
|
+
if (m === 'plan' || m === 'default' || m === 'auto-edit' || m === 'yolo')
|
|
24
|
+
return m;
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
function splitDiscord(text, limit = 1900) {
|
|
28
|
+
if (text.length <= limit)
|
|
29
|
+
return [text];
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < text.length) {
|
|
33
|
+
chunks.push(text.slice(i, i + limit));
|
|
34
|
+
i += limit;
|
|
35
|
+
}
|
|
36
|
+
return chunks;
|
|
37
|
+
}
|
|
38
|
+
function safeContent(text) {
|
|
39
|
+
const t = text.trim();
|
|
40
|
+
return t.length ? t : '(empty response)';
|
|
41
|
+
}
|
|
42
|
+
function sessionKeyForMessage(msg, allowGuilds) {
|
|
43
|
+
if (allowGuilds) {
|
|
44
|
+
// Per-channel+user session in guilds so multiple users can safely coexist.
|
|
45
|
+
return `${msg.channelId}:${msg.author.id}`;
|
|
46
|
+
}
|
|
47
|
+
// DM-only mode uses user id as session key.
|
|
48
|
+
return msg.author.id;
|
|
49
|
+
}
|
|
50
|
+
export async function startDiscordBot(config, botConfig) {
|
|
51
|
+
const token = process.env.IDLEHANDS_DISCORD_TOKEN || botConfig.token;
|
|
52
|
+
if (!token) {
|
|
53
|
+
console.error('[bot:discord] Missing token. Set IDLEHANDS_DISCORD_TOKEN or bot.discord.token.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const allowedUsers = parseAllowedUsers(botConfig);
|
|
57
|
+
if (allowedUsers.size === 0) {
|
|
58
|
+
console.error('[bot:discord] bot.discord.allowed_users is empty — refusing to start unauthenticated bot.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const allowGuilds = botConfig.allow_guilds ?? false;
|
|
62
|
+
const guildId = botConfig.guild_id;
|
|
63
|
+
const maxSessions = botConfig.max_sessions ?? 5;
|
|
64
|
+
const maxQueue = botConfig.max_queue ?? 3;
|
|
65
|
+
const sessionTimeoutMs = (botConfig.session_timeout_min ?? 30) * 60_000;
|
|
66
|
+
const approvalMode = normalizeApprovalMode(botConfig.approval_mode, config.approval_mode ?? 'auto-edit');
|
|
67
|
+
const defaultDir = botConfig.default_dir || projectDir(config);
|
|
68
|
+
const sessions = new Map();
|
|
69
|
+
const client = new Client({
|
|
70
|
+
intents: [
|
|
71
|
+
GatewayIntentBits.Guilds,
|
|
72
|
+
GatewayIntentBits.GuildMessages,
|
|
73
|
+
GatewayIntentBits.DirectMessages,
|
|
74
|
+
GatewayIntentBits.MessageContent,
|
|
75
|
+
],
|
|
76
|
+
partials: [Partials.Channel],
|
|
77
|
+
});
|
|
78
|
+
async function getOrCreate(msg) {
|
|
79
|
+
const key = sessionKeyForMessage(msg, allowGuilds);
|
|
80
|
+
const existing = sessions.get(key);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.lastActivity = Date.now();
|
|
83
|
+
return existing;
|
|
84
|
+
}
|
|
85
|
+
if (sessions.size >= maxSessions) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const cfg = {
|
|
89
|
+
...config,
|
|
90
|
+
dir: defaultDir,
|
|
91
|
+
approval_mode: approvalMode,
|
|
92
|
+
no_confirm: approvalMode === 'yolo',
|
|
93
|
+
};
|
|
94
|
+
const confirmProvider = new DiscordConfirmProvider(msg.channel, msg.author.id, botConfig.confirm_timeout_sec ?? 300);
|
|
95
|
+
const session = await createSession({
|
|
96
|
+
config: cfg,
|
|
97
|
+
confirmProvider,
|
|
98
|
+
confirm: async () => true,
|
|
99
|
+
});
|
|
100
|
+
const managed = {
|
|
101
|
+
key,
|
|
102
|
+
userId: msg.author.id,
|
|
103
|
+
channel: msg.channel,
|
|
104
|
+
session,
|
|
105
|
+
confirmProvider,
|
|
106
|
+
config: cfg,
|
|
107
|
+
inFlight: false,
|
|
108
|
+
pendingQueue: [],
|
|
109
|
+
state: 'idle',
|
|
110
|
+
activeTurnId: 0,
|
|
111
|
+
activeAbortController: null,
|
|
112
|
+
lastProgressAt: 0,
|
|
113
|
+
lastActivity: Date.now(),
|
|
114
|
+
antonActive: false,
|
|
115
|
+
antonAbortSignal: null,
|
|
116
|
+
antonLastResult: null,
|
|
117
|
+
antonProgress: null,
|
|
118
|
+
};
|
|
119
|
+
sessions.set(key, managed);
|
|
120
|
+
return managed;
|
|
121
|
+
}
|
|
122
|
+
function destroySession(key) {
|
|
123
|
+
const s = sessions.get(key);
|
|
124
|
+
if (!s)
|
|
125
|
+
return;
|
|
126
|
+
s.state = 'resetting';
|
|
127
|
+
s.pendingQueue = [];
|
|
128
|
+
try {
|
|
129
|
+
s.activeAbortController?.abort();
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
try {
|
|
133
|
+
s.session.cancel();
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
sessions.delete(key);
|
|
137
|
+
}
|
|
138
|
+
const cleanupTimer = setInterval(() => {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
for (const [key, s] of sessions) {
|
|
141
|
+
if (!s.inFlight && now - s.lastActivity > sessionTimeoutMs) {
|
|
142
|
+
destroySession(key);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}, 60_000);
|
|
146
|
+
function beginTurn(managed) {
|
|
147
|
+
if (managed.inFlight || managed.state === 'resetting')
|
|
148
|
+
return null;
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
managed.inFlight = true;
|
|
151
|
+
managed.state = 'running';
|
|
152
|
+
managed.activeTurnId += 1;
|
|
153
|
+
managed.activeAbortController = controller;
|
|
154
|
+
managed.lastProgressAt = Date.now();
|
|
155
|
+
managed.lastActivity = Date.now();
|
|
156
|
+
return { turnId: managed.activeTurnId, controller };
|
|
157
|
+
}
|
|
158
|
+
function isTurnActive(managed, turnId) {
|
|
159
|
+
return managed.inFlight && managed.activeTurnId == turnId && managed.state !== 'resetting';
|
|
160
|
+
}
|
|
161
|
+
function markProgress(managed, turnId) {
|
|
162
|
+
if (managed.activeTurnId !== turnId)
|
|
163
|
+
return;
|
|
164
|
+
managed.lastProgressAt = Date.now();
|
|
165
|
+
managed.lastActivity = Date.now();
|
|
166
|
+
}
|
|
167
|
+
function finishTurn(managed, turnId) {
|
|
168
|
+
if (managed.activeTurnId !== turnId)
|
|
169
|
+
return;
|
|
170
|
+
managed.inFlight = false;
|
|
171
|
+
managed.state = 'idle';
|
|
172
|
+
managed.activeAbortController = null;
|
|
173
|
+
managed.lastActivity = Date.now();
|
|
174
|
+
}
|
|
175
|
+
function cancelActive(managed) {
|
|
176
|
+
if (!managed.inFlight)
|
|
177
|
+
return { ok: false, message: 'Nothing running.' };
|
|
178
|
+
managed.state = 'canceling';
|
|
179
|
+
managed.pendingQueue = [];
|
|
180
|
+
try {
|
|
181
|
+
managed.activeAbortController?.abort();
|
|
182
|
+
}
|
|
183
|
+
catch { }
|
|
184
|
+
try {
|
|
185
|
+
managed.session.cancel();
|
|
186
|
+
}
|
|
187
|
+
catch { }
|
|
188
|
+
managed.lastActivity = Date.now();
|
|
189
|
+
return { ok: true, message: '⏹ Cancel requested. Stopping current turn...' };
|
|
190
|
+
}
|
|
191
|
+
async function processMessage(managed, msg) {
|
|
192
|
+
const turn = beginTurn(managed);
|
|
193
|
+
if (!turn)
|
|
194
|
+
return;
|
|
195
|
+
const turnId = turn.turnId;
|
|
196
|
+
const placeholder = await msg.reply('⏳ Thinking...').catch(() => null);
|
|
197
|
+
let streamed = '';
|
|
198
|
+
const hooks = {
|
|
199
|
+
onToken: (t) => {
|
|
200
|
+
if (!isTurnActive(managed, turnId))
|
|
201
|
+
return;
|
|
202
|
+
markProgress(managed, turnId);
|
|
203
|
+
streamed += t;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const watchdogMs = 120_000;
|
|
207
|
+
const watchdog = setInterval(() => {
|
|
208
|
+
if (!isTurnActive(managed, turnId))
|
|
209
|
+
return;
|
|
210
|
+
if (Date.now() - managed.lastProgressAt > watchdogMs) {
|
|
211
|
+
console.error(`[bot:discord] ${managed.userId} watchdog timeout on turn ${turnId}`);
|
|
212
|
+
cancelActive(managed);
|
|
213
|
+
}
|
|
214
|
+
}, 5_000);
|
|
215
|
+
try {
|
|
216
|
+
const result = await managed.session.ask(msg.content, { ...hooks, signal: turn.controller.signal });
|
|
217
|
+
if (!isTurnActive(managed, turnId))
|
|
218
|
+
return;
|
|
219
|
+
markProgress(managed, turnId);
|
|
220
|
+
const finalText = safeContent(streamed || result.text);
|
|
221
|
+
const chunks = splitDiscord(finalText);
|
|
222
|
+
if (placeholder) {
|
|
223
|
+
await placeholder.edit(chunks[0]).catch(() => { });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
await msg.reply(chunks[0]).catch(() => { });
|
|
227
|
+
}
|
|
228
|
+
for (let i = 1; i < chunks.length && i < 10; i++) {
|
|
229
|
+
if (!isTurnActive(managed, turnId))
|
|
230
|
+
break;
|
|
231
|
+
await msg.channel.send(chunks[i]).catch(() => { });
|
|
232
|
+
}
|
|
233
|
+
if (chunks.length > 10 && isTurnActive(managed, turnId)) {
|
|
234
|
+
await msg.channel.send('[truncated — response too long]').catch(() => { });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
const raw = String(e?.message ?? e ?? 'unknown error');
|
|
239
|
+
if (!isTurnActive(managed, turnId))
|
|
240
|
+
return;
|
|
241
|
+
if (raw.includes('AbortError') || raw.toLowerCase().includes('aborted')) {
|
|
242
|
+
if (placeholder)
|
|
243
|
+
await placeholder.edit('⏹ Cancelled.').catch(() => { });
|
|
244
|
+
else
|
|
245
|
+
await msg.reply('⏹ Cancelled.').catch(() => { });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const errMsg = raw.slice(0, 400);
|
|
249
|
+
if (placeholder) {
|
|
250
|
+
await placeholder.edit(`❌ ${errMsg}`).catch(() => { });
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await msg.reply(`❌ ${errMsg}`).catch(() => { });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
clearInterval(watchdog);
|
|
259
|
+
finishTurn(managed, turnId);
|
|
260
|
+
const next = managed.pendingQueue.shift();
|
|
261
|
+
if (next && managed.state === 'idle' && !managed.inFlight) {
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
if (managed.state !== 'idle' || managed.inFlight)
|
|
264
|
+
return;
|
|
265
|
+
void processMessage(managed, next);
|
|
266
|
+
}, 200);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function recreateSession(managed, cfg) {
|
|
271
|
+
managed.state = 'resetting';
|
|
272
|
+
managed.pendingQueue = [];
|
|
273
|
+
try {
|
|
274
|
+
managed.activeAbortController?.abort();
|
|
275
|
+
}
|
|
276
|
+
catch { }
|
|
277
|
+
try {
|
|
278
|
+
managed.session.cancel();
|
|
279
|
+
}
|
|
280
|
+
catch { }
|
|
281
|
+
const session = await createSession({
|
|
282
|
+
config: cfg,
|
|
283
|
+
confirmProvider: managed.confirmProvider,
|
|
284
|
+
confirm: async () => true,
|
|
285
|
+
});
|
|
286
|
+
managed.session = session;
|
|
287
|
+
managed.config = cfg;
|
|
288
|
+
managed.inFlight = false;
|
|
289
|
+
managed.state = 'idle';
|
|
290
|
+
managed.activeAbortController = null;
|
|
291
|
+
managed.lastProgressAt = 0;
|
|
292
|
+
managed.lastActivity = Date.now();
|
|
293
|
+
}
|
|
294
|
+
client.on(Events.ClientReady, () => {
|
|
295
|
+
console.error(`[bot:discord] Connected as ${client.user?.tag ?? 'unknown'}`);
|
|
296
|
+
console.error(`[bot:discord] Allowed users: [${[...allowedUsers].join(', ')}]`);
|
|
297
|
+
console.error(`[bot:discord] Default dir: ${defaultDir}`);
|
|
298
|
+
console.error(`[bot:discord] Approval: ${approvalMode}`);
|
|
299
|
+
if (allowGuilds) {
|
|
300
|
+
console.error(`[bot:discord] Guild mode enabled${guildId ? ` (guild ${guildId})` : ''}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
client.on(Events.MessageCreate, async (msg) => {
|
|
304
|
+
if (msg.author.bot)
|
|
305
|
+
return;
|
|
306
|
+
if (!allowedUsers.has(msg.author.id))
|
|
307
|
+
return;
|
|
308
|
+
if (!allowGuilds && msg.guildId)
|
|
309
|
+
return;
|
|
310
|
+
if (allowGuilds && guildId && msg.guildId && msg.guildId !== guildId)
|
|
311
|
+
return;
|
|
312
|
+
const content = msg.content?.trim();
|
|
313
|
+
if (!content)
|
|
314
|
+
return;
|
|
315
|
+
const key = sessionKeyForMessage(msg, allowGuilds);
|
|
316
|
+
if (content === '/reset') {
|
|
317
|
+
destroySession(key);
|
|
318
|
+
await msg.reply('Session reset.').catch(() => { });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const managed = await getOrCreate(msg);
|
|
322
|
+
if (!managed) {
|
|
323
|
+
await msg.reply('⚠️ Too many active sessions. Please retry later.').catch(() => { });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (content === '/cancel') {
|
|
327
|
+
const res = cancelActive(managed);
|
|
328
|
+
await msg.reply(res.message).catch(() => { });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (content === '/start') {
|
|
332
|
+
const lines = [
|
|
333
|
+
'🔧 Idle Hands — Local-first coding agent',
|
|
334
|
+
'',
|
|
335
|
+
`Model: \`${managed.session.model}\``,
|
|
336
|
+
`Endpoint: \`${managed.config.endpoint || '?'}\``,
|
|
337
|
+
`Default dir: \`${managed.config.dir || defaultDir}\``,
|
|
338
|
+
'',
|
|
339
|
+
'Send me a coding task, or use /help for commands.',
|
|
340
|
+
];
|
|
341
|
+
await msg.reply(lines.join('\n')).catch(() => { });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (content === '/help') {
|
|
345
|
+
const lines = [
|
|
346
|
+
'Commands:',
|
|
347
|
+
'/start — Welcome + config summary',
|
|
348
|
+
'/help — This message',
|
|
349
|
+
'/reset — Clear session, start fresh',
|
|
350
|
+
'/cancel — Abort current generation',
|
|
351
|
+
'/status — Session stats',
|
|
352
|
+
'/dir [path] — Get/set working directory',
|
|
353
|
+
'/model — Show current model',
|
|
354
|
+
'/approval [mode] — Get/set approval mode',
|
|
355
|
+
'/mode [code|sys] — Get/set mode',
|
|
356
|
+
'/compact — Trigger context compaction',
|
|
357
|
+
'/changes — Show files modified this session',
|
|
358
|
+
'/undo — Undo last edit',
|
|
359
|
+
'/vault <query> — Search vault entries',
|
|
360
|
+
'/anton <file> — Start autonomous task runner',
|
|
361
|
+
'/anton status | /anton stop | /anton last',
|
|
362
|
+
];
|
|
363
|
+
await msg.reply(lines.join('\n')).catch(() => { });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (content === '/model') {
|
|
367
|
+
await msg.reply(`Model: \`${managed.session.model}\`\nHarness: \`${managed.session.harness}\``).catch(() => { });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (content === '/compact') {
|
|
371
|
+
managed.session.reset();
|
|
372
|
+
await msg.reply('🗜 Session context compacted (reset to system prompt).').catch(() => { });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (content === '/dir' || content.startsWith('/dir ')) {
|
|
376
|
+
const arg = content.slice('/dir'.length).trim();
|
|
377
|
+
if (!arg) {
|
|
378
|
+
await msg.reply(`Working directory: \`${managed.config.dir || defaultDir}\``).catch(() => { });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const allowedDirs = botConfig.allowed_dirs ?? ['~'];
|
|
382
|
+
const homeDir = process.env.HOME || '/home';
|
|
383
|
+
const resolvedDir = arg.replace(/^~/, homeDir);
|
|
384
|
+
const allowed = allowedDirs.some((d) => resolvedDir.startsWith(d.replace(/^~/, homeDir)));
|
|
385
|
+
if (!allowed) {
|
|
386
|
+
await msg.reply('❌ Directory not allowed. Check bot.discord.allowed_dirs.').catch(() => { });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const cfg = {
|
|
390
|
+
...managed.config,
|
|
391
|
+
dir: resolvedDir,
|
|
392
|
+
};
|
|
393
|
+
await recreateSession(managed, cfg);
|
|
394
|
+
await msg.reply(`✅ Working directory set to \`${resolvedDir}\``).catch(() => { });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (content === '/approval' || content.startsWith('/approval ')) {
|
|
398
|
+
const arg = content.slice('/approval'.length).trim().toLowerCase();
|
|
399
|
+
const modes = ['plan', 'default', 'auto-edit', 'yolo'];
|
|
400
|
+
if (!arg) {
|
|
401
|
+
await msg.reply(`Approval mode: \`${managed.config.approval_mode || approvalMode}\`\nOptions: ${modes.join(', ')}`).catch(() => { });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!modes.includes(arg)) {
|
|
405
|
+
await msg.reply(`Invalid mode. Options: ${modes.join(', ')}`).catch(() => { });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
managed.config.approval_mode = arg;
|
|
409
|
+
managed.config.no_confirm = arg === 'yolo';
|
|
410
|
+
await msg.reply(`✅ Approval mode set to \`${arg}\``).catch(() => { });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (content === '/mode' || content.startsWith('/mode ')) {
|
|
414
|
+
const arg = content.slice('/mode'.length).trim().toLowerCase();
|
|
415
|
+
if (!arg) {
|
|
416
|
+
await msg.reply(`Mode: \`${managed.config.mode || 'code'}\``).catch(() => { });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (arg !== 'code' && arg !== 'sys') {
|
|
420
|
+
await msg.reply('Invalid mode. Options: code, sys').catch(() => { });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
managed.config.mode = arg;
|
|
424
|
+
if (arg === 'sys' && managed.config.approval_mode === 'auto-edit') {
|
|
425
|
+
managed.config.approval_mode = 'default';
|
|
426
|
+
}
|
|
427
|
+
await msg.reply(`✅ Mode set to \`${arg}\``).catch(() => { });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (content === '/changes') {
|
|
431
|
+
const replay = managed.session.replay;
|
|
432
|
+
if (!replay) {
|
|
433
|
+
await msg.reply('Replay is disabled. No change tracking available.').catch(() => { });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const checkpoints = await replay.list(50);
|
|
438
|
+
if (!checkpoints.length) {
|
|
439
|
+
await msg.reply('No file changes this session.').catch(() => { });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const byFile = new Map();
|
|
443
|
+
for (const cp of checkpoints)
|
|
444
|
+
byFile.set(cp.filePath, (byFile.get(cp.filePath) ?? 0) + 1);
|
|
445
|
+
const lines = [`Session changes (${byFile.size} files):`];
|
|
446
|
+
for (const [fp, count] of byFile)
|
|
447
|
+
lines.push(`✎ \`${fp}\` (${count} edit${count > 1 ? 's' : ''})`);
|
|
448
|
+
await msg.reply(lines.join('\n')).catch(() => { });
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
await msg.reply(`Error listing changes: ${e?.message ?? String(e)}`).catch(() => { });
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (content === '/undo') {
|
|
456
|
+
const lastPath = managed.session.lastEditedPath;
|
|
457
|
+
if (!lastPath) {
|
|
458
|
+
await msg.reply('No recent edits to undo.').catch(() => { });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const { undo_path } = await import('../tools.js');
|
|
463
|
+
const result = await undo_path({ cwd: managed.config.dir || defaultDir, noConfirm: true, dryRun: false }, { path: lastPath });
|
|
464
|
+
await msg.reply(`✅ ${result}`).catch(() => { });
|
|
465
|
+
}
|
|
466
|
+
catch (e) {
|
|
467
|
+
await msg.reply(`❌ Undo failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (content === '/vault' || content.startsWith('/vault ')) {
|
|
472
|
+
const query = content.slice('/vault'.length).trim();
|
|
473
|
+
if (!query) {
|
|
474
|
+
await msg.reply('Usage: /vault <search query>').catch(() => { });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const vault = managed.session.vault;
|
|
478
|
+
if (!vault) {
|
|
479
|
+
await msg.reply('Vault is disabled.').catch(() => { });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const results = await vault.search(query, 5);
|
|
484
|
+
if (!results.length) {
|
|
485
|
+
await msg.reply(`No vault results for "${query}"`).catch(() => { });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const lines = [`Vault results for "${query}":`];
|
|
489
|
+
for (const r of results) {
|
|
490
|
+
const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || '?'}`;
|
|
491
|
+
const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 120);
|
|
492
|
+
lines.push(`• ${title}: ${body}`);
|
|
493
|
+
}
|
|
494
|
+
await msg.reply(lines.join('\n')).catch(() => { });
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
await msg.reply(`Error searching vault: ${e?.message ?? String(e)}`).catch(() => { });
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (content === '/status') {
|
|
502
|
+
const used = managed.session.usage.prompt + managed.session.usage.completion;
|
|
503
|
+
const pct = managed.session.contextWindow > 0
|
|
504
|
+
? ((used / managed.session.contextWindow) * 100).toFixed(1)
|
|
505
|
+
: '?';
|
|
506
|
+
await msg.reply([
|
|
507
|
+
`Mode: ${managed.config.mode ?? 'code'}`,
|
|
508
|
+
`Approval: ${managed.config.approval_mode}`,
|
|
509
|
+
`Model: ${managed.session.model}`,
|
|
510
|
+
`Harness: ${managed.session.harness}`,
|
|
511
|
+
`Context: ~${used}/${managed.session.contextWindow} (${pct}%)`,
|
|
512
|
+
`State: ${managed.state}`,
|
|
513
|
+
`Queue: ${managed.pendingQueue.length}/${maxQueue}`,
|
|
514
|
+
].join('\n')).catch(() => { });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (content === '/hosts') {
|
|
518
|
+
try {
|
|
519
|
+
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
520
|
+
const config = await loadRuntimes();
|
|
521
|
+
const redacted = redactConfig(config);
|
|
522
|
+
if (!redacted.hosts.length) {
|
|
523
|
+
await msg.reply('No hosts configured. Use `idlehands hosts add` in CLI.').catch(() => { });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const lines = redacted.hosts.map((h) => `${h.enabled ? '🟢' : '🔴'} ${h.display_name} (\`${h.id}\`)\n Transport: ${h.transport}`);
|
|
527
|
+
const chunks = splitDiscord(lines.join('\n\n'));
|
|
528
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
529
|
+
if (i === 0)
|
|
530
|
+
await msg.reply(chunk).catch(() => { });
|
|
531
|
+
else
|
|
532
|
+
await msg.channel.send(chunk).catch(() => { });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (e) {
|
|
536
|
+
await msg.reply(`❌ Failed to load hosts: ${e?.message ?? String(e)}`).catch(() => { });
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (content === '/backends') {
|
|
541
|
+
try {
|
|
542
|
+
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|
|
543
|
+
const config = await loadRuntimes();
|
|
544
|
+
const redacted = redactConfig(config);
|
|
545
|
+
if (!redacted.backends.length) {
|
|
546
|
+
await msg.reply('No backends configured. Use `idlehands backends add` in CLI.').catch(() => { });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const lines = redacted.backends.map((b) => `${b.enabled ? '🟢' : '🔴'} ${b.display_name} (\`${b.id}\`)\n Type: ${b.type}`);
|
|
550
|
+
const chunks = splitDiscord(lines.join('\n\n'));
|
|
551
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
552
|
+
if (i === 0)
|
|
553
|
+
await msg.reply(chunk).catch(() => { });
|
|
554
|
+
else
|
|
555
|
+
await msg.channel.send(chunk).catch(() => { });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
await msg.reply(`❌ Failed to load backends: ${e?.message ?? String(e)}`).catch(() => { });
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (content === '/rtmodels') {
|
|
564
|
+
try {
|
|
565
|
+
const { loadRuntimes } = await import('../runtime/store.js');
|
|
566
|
+
const config = await loadRuntimes();
|
|
567
|
+
if (!config.models.length) {
|
|
568
|
+
await msg.reply('No runtime models configured.').catch(() => { });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const lines = config.models.map((m) => `${m.enabled ? '🟢' : '🔴'} ${m.display_name} (\`${m.id}\`)\n Source: \`${m.source}\``);
|
|
572
|
+
const chunks = splitDiscord(lines.join('\n\n'));
|
|
573
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
574
|
+
if (i === 0)
|
|
575
|
+
await msg.reply(chunk).catch(() => { });
|
|
576
|
+
else
|
|
577
|
+
await msg.channel.send(chunk).catch(() => { });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (e) {
|
|
581
|
+
await msg.reply(`❌ Failed to load runtime models: ${e?.message ?? String(e)}`).catch(() => { });
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (content === '/rtstatus') {
|
|
586
|
+
try {
|
|
587
|
+
const { loadActiveRuntime } = await import('../runtime/executor.js');
|
|
588
|
+
const active = await loadActiveRuntime();
|
|
589
|
+
if (!active) {
|
|
590
|
+
await msg.reply('No active runtime.').catch(() => { });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const lines = [
|
|
594
|
+
'Active Runtime',
|
|
595
|
+
`Model: \`${active.modelId}\``,
|
|
596
|
+
`Backend: \`${active.backendId ?? 'none'}\``,
|
|
597
|
+
`Hosts: ${active.hostIds.map((id) => `\`${id}\``).join(', ') || 'none'}`,
|
|
598
|
+
`Healthy: ${active.healthy ? '✅ yes' : '❌ no'}`,
|
|
599
|
+
`Endpoint: \`${active.endpoint ?? 'unknown'}\``,
|
|
600
|
+
`Started: \`${active.startedAt}\``,
|
|
601
|
+
];
|
|
602
|
+
const chunks = splitDiscord(lines.join('\n'));
|
|
603
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
604
|
+
if (i === 0)
|
|
605
|
+
await msg.reply(chunk).catch(() => { });
|
|
606
|
+
else
|
|
607
|
+
await msg.channel.send(chunk).catch(() => { });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
await msg.reply(`❌ Failed to read runtime status: ${e?.message ?? String(e)}`).catch(() => { });
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (content === '/switch' || content.startsWith('/switch ')) {
|
|
616
|
+
try {
|
|
617
|
+
const modelId = content.slice('/switch'.length).trim();
|
|
618
|
+
if (!modelId) {
|
|
619
|
+
await msg.reply('Usage: /switch <model-id>').catch(() => { });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const { plan } = await import('../runtime/planner.js');
|
|
623
|
+
const { execute, loadActiveRuntime } = await import('../runtime/executor.js');
|
|
624
|
+
const { loadRuntimes } = await import('../runtime/store.js');
|
|
625
|
+
const rtConfig = await loadRuntimes();
|
|
626
|
+
const active = await loadActiveRuntime();
|
|
627
|
+
const result = plan({ modelId, mode: 'live' }, rtConfig, active);
|
|
628
|
+
if (!result.ok) {
|
|
629
|
+
await msg.reply(`❌ Plan failed: ${result.reason}`).catch(() => { });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (result.reuse) {
|
|
633
|
+
await msg.reply('✅ Runtime already active and healthy.').catch(() => { });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const statusMsg = await msg.reply(`⏳ Switching to \`${result.model.display_name}\`...`).catch(() => null);
|
|
637
|
+
const execResult = await execute(result, {
|
|
638
|
+
onStep: async (step, status) => {
|
|
639
|
+
if (status === 'done' && statusMsg) {
|
|
640
|
+
await statusMsg.edit(`⏳ ${step.description}... ✓`).catch(() => { });
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
confirm: async (prompt) => {
|
|
644
|
+
await msg.reply(`⚠️ ${prompt}\nAuto-approving for bot context.`).catch(() => { });
|
|
645
|
+
return true;
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
if (execResult.ok) {
|
|
649
|
+
if (statusMsg) {
|
|
650
|
+
await statusMsg.edit(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
await msg.reply(`✅ Switched to \`${result.model.display_name}\``).catch(() => { });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
const err = `❌ Switch failed: ${execResult.error || 'unknown error'}`;
|
|
658
|
+
if (statusMsg) {
|
|
659
|
+
await statusMsg.edit(err).catch(() => { });
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
await msg.reply(err).catch(() => { });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
catch (e) {
|
|
667
|
+
await msg.reply(`❌ Switch failed: ${e?.message ?? String(e)}`).catch(() => { });
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// /anton command
|
|
672
|
+
if (content === '/anton' || content.startsWith('/anton ')) {
|
|
673
|
+
await handleDiscordAnton(managed, msg, content);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (managed.inFlight) {
|
|
677
|
+
if (managed.pendingQueue.length >= maxQueue) {
|
|
678
|
+
await msg.reply(`⏳ Queue full (${managed.pendingQueue.length}/${maxQueue}). Use /cancel.`).catch(() => { });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
managed.pendingQueue.push(msg);
|
|
682
|
+
await msg.reply(`⏳ Queued (#${managed.pendingQueue.length}).`).catch(() => { });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
console.error(`[bot:discord] ${msg.author.id}: ${content.slice(0, 50)}${content.length > 50 ? '…' : ''}`);
|
|
686
|
+
await processMessage(managed, msg);
|
|
687
|
+
});
|
|
688
|
+
const DISCORD_RATE_LIMIT_MS = 15_000;
|
|
689
|
+
async function handleDiscordAnton(managed, msg, content) {
|
|
690
|
+
const args = content.replace(/^\/anton\s*/, '').trim();
|
|
691
|
+
const sub = args.split(/\s+/)[0]?.toLowerCase() || '';
|
|
692
|
+
if (!sub || sub === 'status') {
|
|
693
|
+
if (!managed.antonActive) {
|
|
694
|
+
await msg.reply('No Anton run in progress.').catch(() => { });
|
|
695
|
+
}
|
|
696
|
+
else if (managed.antonProgress) {
|
|
697
|
+
await msg.reply(formatProgressBar(managed.antonProgress)).catch(() => { });
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
await msg.reply('🤖 Anton is running (no progress data yet).').catch(() => { });
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (sub === 'stop') {
|
|
705
|
+
if (!managed.antonActive || !managed.antonAbortSignal) {
|
|
706
|
+
await msg.reply('No Anton run in progress.').catch(() => { });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
managed.antonAbortSignal.aborted = true;
|
|
710
|
+
await msg.reply('🛑 Anton stop requested.').catch(() => { });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (sub === 'last') {
|
|
714
|
+
if (!managed.antonLastResult) {
|
|
715
|
+
await msg.reply('No previous Anton run.').catch(() => { });
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
await msg.reply(formatRunSummary(managed.antonLastResult)).catch(() => { });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const filePart = sub === 'run' ? args.replace(/^\S+\s*/, '').trim() : args;
|
|
722
|
+
if (!filePart) {
|
|
723
|
+
await msg.reply('/anton <file> — start | /anton status | /anton stop | /anton last').catch(() => { });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (managed.antonActive) {
|
|
727
|
+
await msg.reply('⚠️ Anton is already running. Use /anton stop first.').catch(() => { });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const cwd = managed.config.dir || process.cwd();
|
|
731
|
+
const filePath = path.resolve(cwd, filePart);
|
|
732
|
+
try {
|
|
733
|
+
await fs.stat(filePath);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
await msg.reply(`File not found: ${filePath}`).catch(() => { });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const defaults = managed.config.anton || {};
|
|
740
|
+
const runConfig = {
|
|
741
|
+
taskFile: filePath, projectDir: cwd,
|
|
742
|
+
maxRetriesPerTask: defaults.max_retries ?? 3,
|
|
743
|
+
maxIterations: defaults.max_iterations ?? 200,
|
|
744
|
+
taskTimeoutSec: defaults.task_timeout_sec ?? 600,
|
|
745
|
+
totalTimeoutSec: defaults.total_timeout_sec ?? 7200,
|
|
746
|
+
maxTotalTokens: defaults.max_total_tokens ?? Infinity,
|
|
747
|
+
autoCommit: defaults.auto_commit ?? true,
|
|
748
|
+
branch: false, allowDirty: false,
|
|
749
|
+
aggressiveCleanOnFail: false,
|
|
750
|
+
verifyAi: defaults.verify_ai ?? true,
|
|
751
|
+
verifyModel: undefined,
|
|
752
|
+
decompose: defaults.decompose ?? true,
|
|
753
|
+
maxDecomposeDepth: defaults.max_decompose_depth ?? 2,
|
|
754
|
+
maxTotalTasks: defaults.max_total_tasks ?? 500,
|
|
755
|
+
buildCommand: undefined, testCommand: undefined, lintCommand: undefined,
|
|
756
|
+
skipOnFail: defaults.skip_on_fail ?? true,
|
|
757
|
+
approvalMode: (defaults.approval_mode ?? 'yolo'),
|
|
758
|
+
verbose: false, dryRun: false,
|
|
759
|
+
};
|
|
760
|
+
const abortSignal = { aborted: false };
|
|
761
|
+
managed.antonActive = true;
|
|
762
|
+
managed.antonAbortSignal = abortSignal;
|
|
763
|
+
managed.antonProgress = null;
|
|
764
|
+
let lastProgressAt = 0;
|
|
765
|
+
const channel = msg.channel;
|
|
766
|
+
const progress = {
|
|
767
|
+
onTaskStart(task, attempt, prog) {
|
|
768
|
+
managed.antonProgress = prog;
|
|
769
|
+
const now = Date.now();
|
|
770
|
+
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
771
|
+
lastProgressAt = now;
|
|
772
|
+
channel.send(formatTaskStart(task, attempt, prog)).catch(() => { });
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
onTaskEnd(task, result, prog) {
|
|
776
|
+
managed.antonProgress = prog;
|
|
777
|
+
const now = Date.now();
|
|
778
|
+
if (now - lastProgressAt >= DISCORD_RATE_LIMIT_MS) {
|
|
779
|
+
lastProgressAt = now;
|
|
780
|
+
channel.send(formatTaskEnd(task, result, prog)).catch(() => { });
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
onTaskSkip(task, reason) {
|
|
784
|
+
channel.send(formatTaskSkip(task, reason)).catch(() => { });
|
|
785
|
+
},
|
|
786
|
+
onRunComplete(result) {
|
|
787
|
+
managed.antonLastResult = result;
|
|
788
|
+
managed.antonActive = false;
|
|
789
|
+
managed.antonAbortSignal = null;
|
|
790
|
+
managed.antonProgress = null;
|
|
791
|
+
channel.send(formatRunSummary(result)).catch(() => { });
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
let pendingCount = 0;
|
|
795
|
+
try {
|
|
796
|
+
const tf = await parseTaskFile(filePath);
|
|
797
|
+
pendingCount = tf.pending.length;
|
|
798
|
+
}
|
|
799
|
+
catch { }
|
|
800
|
+
await msg.reply(`🤖 Anton started on ${filePart} (${pendingCount} tasks pending)`).catch(() => { });
|
|
801
|
+
runAnton({
|
|
802
|
+
config: runConfig,
|
|
803
|
+
idlehandsConfig: managed.config,
|
|
804
|
+
progress,
|
|
805
|
+
abortSignal,
|
|
806
|
+
vault: managed.session.vault,
|
|
807
|
+
lens: managed.session.lens,
|
|
808
|
+
}).catch((err) => {
|
|
809
|
+
managed.antonActive = false;
|
|
810
|
+
managed.antonAbortSignal = null;
|
|
811
|
+
managed.antonProgress = null;
|
|
812
|
+
channel.send(`Anton error: ${err.message}`).catch(() => { });
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const shutdown = async () => {
|
|
816
|
+
clearInterval(cleanupTimer);
|
|
817
|
+
for (const key of sessions.keys())
|
|
818
|
+
destroySession(key);
|
|
819
|
+
await client.destroy();
|
|
820
|
+
process.exit(0);
|
|
821
|
+
};
|
|
822
|
+
process.on('SIGINT', () => { void shutdown(); });
|
|
823
|
+
process.on('SIGTERM', () => { void shutdown(); });
|
|
824
|
+
await client.login(token);
|
|
825
|
+
}
|
|
826
|
+
//# sourceMappingURL=discord.js.map
|