appback-remoteagent 0.13.0
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/.env.example +39 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/bin/remoteagent.js +2 -0
- package/dist/adapters/claude-adapter.js +78 -0
- package/dist/adapters/codex-adapter.js +241 -0
- package/dist/adapters/provider-adapter.js +1 -0
- package/dist/adapters/shell-adapter.js +44 -0
- package/dist/adapters/windows-shell.js +111 -0
- package/dist/bot.js +2135 -0
- package/dist/config.js +170 -0
- package/dist/index.js +534 -0
- package/dist/secret-helper.js +24 -0
- package/dist/services/agent-memory-service.js +737 -0
- package/dist/services/bot-management-service.js +626 -0
- package/dist/services/bridge-service.js +807 -0
- package/dist/services/local-ui-service.js +533 -0
- package/dist/services/provider-setup-service.js +284 -0
- package/dist/services/remote-shell-service.js +97 -0
- package/dist/store/file-store.js +690 -0
- package/dist/telegram-fetch.js +85 -0
- package/dist/types.js +1 -0
- package/docs/ARCHITECTURE.md +170 -0
- package/docs/COKACDIR_NOTES.md +79 -0
- package/docs/ERROR_NORMALIZATION.md +46 -0
- package/docs/MINI_APP.md +112 -0
- package/docs/MVP.md +108 -0
- package/docs/OPERATIONS.md +181 -0
- package/docs/RELEASING.md +87 -0
- package/docs/SESSION_DIRECTORY_PLAN.md +506 -0
- package/package.json +47 -0
- package/scripts/bump-version.sh +23 -0
- package/scripts/finish-claude-login.sh +48 -0
- package/scripts/install-claude.sh +6 -0
- package/scripts/install-codex.sh +8 -0
- package/scripts/install.ps1 +51 -0
- package/scripts/install.sh +101 -0
- package/scripts/mock-adapter.sh +7 -0
- package/scripts/restart-after-bot-op.sh +118 -0
- package/scripts/selftest-telegram-update.mjs +359 -0
- package/scripts/start-claude-login.sh +4 -0
- package/scripts/start.ps1 +39 -0
- package/scripts/start.sh +54 -0
- package/scripts/stop.ps1 +40 -0
- package/scripts/stop.sh +39 -0
- package/tsconfig.json +20 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { CodexAdapter } from "./adapters/codex-adapter.js";
|
|
8
|
+
import { ClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
9
|
+
import { createBot } from "./bot.js";
|
|
10
|
+
import { ShellAdapter } from "./adapters/shell-adapter.js";
|
|
11
|
+
import { config } from "./config.js";
|
|
12
|
+
import { FileStore } from "./store/file-store.js";
|
|
13
|
+
import { BridgeService } from "./services/bridge-service.js";
|
|
14
|
+
import { BotManagementService } from "./services/bot-management-service.js";
|
|
15
|
+
import { LocalUiService } from "./services/local-ui-service.js";
|
|
16
|
+
import { AgentMemoryService } from "./services/agent-memory-service.js";
|
|
17
|
+
import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const TELEGRAM_GET_UPDATES_HTTP_TIMEOUT_SECONDS = 30;
|
|
20
|
+
const TELEGRAM_GET_UPDATES_CURL_TIMEOUT_SECONDS = 60;
|
|
21
|
+
let processLockPath;
|
|
22
|
+
let telegramTransportStatusPath;
|
|
23
|
+
const telegramTransportStatuses = {};
|
|
24
|
+
let telegramPollingLimiter;
|
|
25
|
+
async function main() {
|
|
26
|
+
processLockPath = await acquireProcessLock(config.dataDir);
|
|
27
|
+
telegramTransportStatusPath = path.join(config.dataDir, "telegram-transport.json");
|
|
28
|
+
telegramPollingLimiter = new AsyncSemaphore(config.telegramPollingMaxConcurrency);
|
|
29
|
+
registerProcessLifecycle();
|
|
30
|
+
const store = new FileStore(config.dataDir, config.defaultMode);
|
|
31
|
+
await store.init();
|
|
32
|
+
const adapters = {
|
|
33
|
+
codex: config.commands.codex
|
|
34
|
+
? new ShellAdapter("codex", config.commands.codex, () => config.commandTimeoutMs)
|
|
35
|
+
: new CodexAdapter(config.codexBin, () => config.commandTimeoutMs, config.codexSandboxMode),
|
|
36
|
+
claude: config.commands.claude
|
|
37
|
+
? new ShellAdapter("claude", config.commands.claude, () => config.commandTimeoutMs)
|
|
38
|
+
: new ClaudeAdapter(config.claudeBin, () => config.commandTimeoutMs, config.claudePermissionMode),
|
|
39
|
+
};
|
|
40
|
+
const isProviderInstalled = (provider) => {
|
|
41
|
+
if (config.commands[provider]) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return provider === "codex"
|
|
45
|
+
? commandExists(config.codexBin)
|
|
46
|
+
: commandExists(config.claudeBin);
|
|
47
|
+
};
|
|
48
|
+
const availableProviders = ["codex", "claude"].filter((provider) => isProviderInstalled(provider));
|
|
49
|
+
console.log(`Available providers: ${availableProviders.length > 0 ? availableProviders.join(", ") : "none"}`);
|
|
50
|
+
const bridge = new BridgeService(store, adapters, config.defaultWorkspace, config.workspaceRoot, isProviderInstalled, config.defaultMode, config.codexSandboxMode);
|
|
51
|
+
const botManagement = new BotManagementService(config.dataDir, config.botRestartServiceName, config.botRestartHelperPath);
|
|
52
|
+
startArtifactCleanupSchedule(new AgentMemoryService(config.dataDir));
|
|
53
|
+
if (config.localUiEnabled) {
|
|
54
|
+
const localUi = new LocalUiService(bridge, config.localUiHost, config.localUiPort);
|
|
55
|
+
await localUi.start()
|
|
56
|
+
.then(() => {
|
|
57
|
+
console.log(`Local UI is ready at http://${config.localUiHost}:${config.localUiPort}`);
|
|
58
|
+
})
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
console.error("Local UI failed to start:", error);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const botInfos = config.telegramBotTokens.map((token, index) => buildBotInfo(token, index));
|
|
64
|
+
const bots = config.telegramBotTokens.map((token, index) => createBot(token, bridge, botManagement, botInfos[index]));
|
|
65
|
+
if (config.telegramCommandMenuEnabled) {
|
|
66
|
+
for (const bot of bots) {
|
|
67
|
+
const username = bot.botInfo.username;
|
|
68
|
+
await configureTelegramCommandMenu(bot).catch((error) => {
|
|
69
|
+
console.error(`Failed to configure command menu for @${username}:`, error);
|
|
70
|
+
});
|
|
71
|
+
console.log(`Bot @${username} is ready`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log("Telegram command menu registration is disabled.");
|
|
76
|
+
for (const bot of bots) {
|
|
77
|
+
console.log(`Bot @${bot.botInfo.username} is ready`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await botManagement.reportPendingOperationResult().catch((error) => {
|
|
81
|
+
console.error("Failed to report pending bot operation result:", error);
|
|
82
|
+
});
|
|
83
|
+
await Promise.all(bots.map((bot, index) => startManualPolling(bot, index)));
|
|
84
|
+
}
|
|
85
|
+
function startArtifactCleanupSchedule(memoryService) {
|
|
86
|
+
if (!config.artifactCleanupEnabled) {
|
|
87
|
+
console.log("Artifact cleanup schedule is disabled.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const run = async () => {
|
|
91
|
+
try {
|
|
92
|
+
const result = await memoryService.cleanupArtifacts(config.artifactRetentionDays);
|
|
93
|
+
console.log(`[artifact-cleanup] ${result}`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error("[artifact-cleanup] failed:", error);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
console.log(`Artifact cleanup schedule enabled: retention=${config.artifactRetentionDays}d interval=${config.artifactCleanupIntervalMs}ms`);
|
|
100
|
+
const initial = setTimeout(() => {
|
|
101
|
+
void run();
|
|
102
|
+
}, 60_000);
|
|
103
|
+
initial.unref();
|
|
104
|
+
const interval = setInterval(() => {
|
|
105
|
+
void run();
|
|
106
|
+
}, config.artifactCleanupIntervalMs);
|
|
107
|
+
interval.unref();
|
|
108
|
+
}
|
|
109
|
+
async function configureTelegramCommandMenu(bot) {
|
|
110
|
+
const commands = [
|
|
111
|
+
{ command: "start", description: "Start a new Codex or Claude session" },
|
|
112
|
+
{ command: "list", description: "List sessions" },
|
|
113
|
+
{ command: "switch", description: "Switch to a session" },
|
|
114
|
+
{ command: "status", description: "Show current session status" },
|
|
115
|
+
{ command: "state", description: "Show or edit session state notes" },
|
|
116
|
+
{ command: "option", description: "Show or change runtime options" },
|
|
117
|
+
{ command: "model", description: "Show or change provider model" },
|
|
118
|
+
{ command: "stop", description: "Stop active work and clear queued messages" },
|
|
119
|
+
{ command: "batch", description: "Collect and send a multi-message batch" },
|
|
120
|
+
{ command: "bots", description: "List configured Telegram bots" },
|
|
121
|
+
{ command: "bot", description: "Manage Telegram bots" },
|
|
122
|
+
{ command: "install", description: "Install or update Codex or Claude" },
|
|
123
|
+
{ command: "login", description: "Run provider login flow" },
|
|
124
|
+
{ command: "reset", description: "Clear this chat binding" },
|
|
125
|
+
{ command: "help", description: "Show command help" },
|
|
126
|
+
];
|
|
127
|
+
const token = bot.token;
|
|
128
|
+
if (!token) {
|
|
129
|
+
throw new Error("Telegram bot token is unavailable for command menu registration.");
|
|
130
|
+
}
|
|
131
|
+
let lastError;
|
|
132
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
133
|
+
try {
|
|
134
|
+
await setTelegramCommandsViaCurl(token, commands);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
lastError = error;
|
|
139
|
+
if (attempt < 3) {
|
|
140
|
+
await sleep(1000 * attempt);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw lastError;
|
|
145
|
+
}
|
|
146
|
+
async function setTelegramCommandsViaCurl(token, commands) {
|
|
147
|
+
const url = `https://api.telegram.org/bot${token}/setMyCommands`;
|
|
148
|
+
const payload = JSON.stringify({ commands });
|
|
149
|
+
let stdout;
|
|
150
|
+
let stderr;
|
|
151
|
+
try {
|
|
152
|
+
const result = await execFileAsync("curl", [
|
|
153
|
+
"-sS",
|
|
154
|
+
"-4",
|
|
155
|
+
"--max-time",
|
|
156
|
+
"20",
|
|
157
|
+
"-H",
|
|
158
|
+
"Content-Type: application/json",
|
|
159
|
+
"-d",
|
|
160
|
+
payload,
|
|
161
|
+
url,
|
|
162
|
+
]);
|
|
163
|
+
stdout = result.stdout;
|
|
164
|
+
stderr = result.stderr;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`Telegram setMyCommands curl failed: ${formatCurlError(error)}`);
|
|
168
|
+
}
|
|
169
|
+
if (stderr?.trim()) {
|
|
170
|
+
console.error(`curl stderr for setMyCommands: ${stderr.trim()}`);
|
|
171
|
+
}
|
|
172
|
+
const parsed = JSON.parse(stdout);
|
|
173
|
+
if (!parsed.ok) {
|
|
174
|
+
throw new Error(parsed.description || "Telegram setMyCommands failed.");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function formatCurlError(error) {
|
|
178
|
+
if (!(error instanceof Error)) {
|
|
179
|
+
return String(error);
|
|
180
|
+
}
|
|
181
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
182
|
+
? String(error.code ?? "")
|
|
183
|
+
: "";
|
|
184
|
+
const stderr = typeof error === "object" && error !== null && "stderr" in error
|
|
185
|
+
? String(error.stderr ?? "").trim()
|
|
186
|
+
: "";
|
|
187
|
+
return [code ? `code=${code}` : undefined, stderr || error.message]
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.join(" ");
|
|
190
|
+
}
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
console.error("RemoteAgent fatal error:", error);
|
|
193
|
+
releaseProcessLockSync();
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
});
|
|
196
|
+
async function startManualPolling(bot, index) {
|
|
197
|
+
const pollingBot = bot;
|
|
198
|
+
let offset = 0;
|
|
199
|
+
let consecutiveFailures = 0;
|
|
200
|
+
let lastFailureLogAt = 0;
|
|
201
|
+
const activeHandlers = new Set();
|
|
202
|
+
const initialDelayMs = Math.min(30_000, index * 3_000 + stableJitterMs(pollingBot.botInfo.username));
|
|
203
|
+
if (initialDelayMs > 0) {
|
|
204
|
+
console.log(`Starting polling for @${pollingBot.botInfo.username} in ${formatDuration(initialDelayMs)}.`);
|
|
205
|
+
await sleep(initialDelayMs);
|
|
206
|
+
}
|
|
207
|
+
while (true) {
|
|
208
|
+
try {
|
|
209
|
+
const payload = await telegramPollingLimiter.run(() => getUpdatesViaCurl(pollingBot.token, offset));
|
|
210
|
+
if (consecutiveFailures > 0) {
|
|
211
|
+
console.warn(`Telegram polling recovered for @${pollingBot.botInfo.username} after ${consecutiveFailures} failure(s).`);
|
|
212
|
+
await writeTelegramTransportStatus(pollingBot.botInfo.username, {
|
|
213
|
+
status: "ok",
|
|
214
|
+
consecutiveFailures: 0,
|
|
215
|
+
lastRecoveredAt: new Date().toISOString(),
|
|
216
|
+
}).catch((error) => {
|
|
217
|
+
console.error(`Failed to write Telegram transport recovery status for @${pollingBot.botInfo.username}:`, error);
|
|
218
|
+
});
|
|
219
|
+
consecutiveFailures = 0;
|
|
220
|
+
lastFailureLogAt = 0;
|
|
221
|
+
}
|
|
222
|
+
if (payload.result.length === 0) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const orderedUpdates = orderUpdatesForDispatch(payload.result);
|
|
226
|
+
const stopUpdates = orderedUpdates.filter((update) => isStopCommandUpdate(update));
|
|
227
|
+
if (stopUpdates.length > 0) {
|
|
228
|
+
for (const update of stopUpdates) {
|
|
229
|
+
await pollingBot.handleUpdates([update]).catch((error) => {
|
|
230
|
+
console.error(`Telegram stop update ${update.update_id} handler failed for @${pollingBot.botInfo.username}:`, error);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const skipped = orderedUpdates.length - stopUpdates.length;
|
|
234
|
+
if (skipped > 0) {
|
|
235
|
+
console.log(`Skipped ${skipped} queued update(s) for @${pollingBot.botInfo.username} because /stop was received in the same polling batch.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
for (const update of orderedUpdates) {
|
|
240
|
+
const handler = pollingBot.handleUpdates([update]).catch((error) => {
|
|
241
|
+
console.error(`Telegram update ${update.update_id} handler failed for @${pollingBot.botInfo.username}:`, error);
|
|
242
|
+
}).finally(() => {
|
|
243
|
+
activeHandlers.delete(handler);
|
|
244
|
+
});
|
|
245
|
+
activeHandlers.add(handler);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
offset = payload.result[payload.result.length - 1].update_id + 1;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
consecutiveFailures += 1;
|
|
252
|
+
const issue = summarizeTelegramTransportError(error);
|
|
253
|
+
const delayMs = Math.max(nextPollingBackoffMs(consecutiveFailures, pollingBot.botInfo.username), getRetryAfterMs(error) ?? 0);
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
if (consecutiveFailures === 1 || now - lastFailureLogAt >= 60_000) {
|
|
256
|
+
lastFailureLogAt = now;
|
|
257
|
+
console.error(`Polling failed for @${pollingBot.botInfo.username}: ${issue}. `
|
|
258
|
+
+ `consecutiveFailures=${consecutiveFailures}; nextRetryIn=${formatDuration(delayMs)}.`);
|
|
259
|
+
}
|
|
260
|
+
await writeTelegramTransportStatus(pollingBot.botInfo.username, {
|
|
261
|
+
status: "degraded",
|
|
262
|
+
consecutiveFailures,
|
|
263
|
+
lastIssue: issue,
|
|
264
|
+
lastFailureAt: new Date().toISOString(),
|
|
265
|
+
nextRetryAt: new Date(Date.now() + delayMs).toISOString(),
|
|
266
|
+
}).catch((statusError) => {
|
|
267
|
+
console.error(`Failed to write Telegram transport failure status for @${pollingBot.botInfo.username}:`, statusError);
|
|
268
|
+
});
|
|
269
|
+
await sleep(delayMs);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function orderUpdatesForDispatch(updates) {
|
|
274
|
+
return [...updates].sort((left, right) => {
|
|
275
|
+
const leftStop = isStopCommandUpdate(left) ? 0 : 1;
|
|
276
|
+
const rightStop = isStopCommandUpdate(right) ? 0 : 1;
|
|
277
|
+
return leftStop - rightStop || left.update_id - right.update_id;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function isStopCommandUpdate(update) {
|
|
281
|
+
const text = update.message?.text ?? update.edited_message?.text ?? update.channel_post?.text ?? "";
|
|
282
|
+
return /^\/stop(?:@\w+)?(?:\s|$)/i.test(text.trim());
|
|
283
|
+
}
|
|
284
|
+
function sleep(ms) {
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
setTimeout(resolve, ms);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function nextPollingBackoffMs(failureCount, username) {
|
|
290
|
+
const exponent = Math.max(0, Math.min(failureCount - 1, 10));
|
|
291
|
+
const baseDelay = Math.min(config.telegramPollingBackoffMaxMs, config.telegramPollingBackoffMinMs * (2 ** exponent));
|
|
292
|
+
return Math.min(config.telegramPollingBackoffMaxMs, baseDelay + stableJitterMs(username));
|
|
293
|
+
}
|
|
294
|
+
function stableJitterMs(value) {
|
|
295
|
+
const source = value || "unknown";
|
|
296
|
+
let hash = 0;
|
|
297
|
+
for (const char of source) {
|
|
298
|
+
hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
|
|
299
|
+
}
|
|
300
|
+
return hash % 5000;
|
|
301
|
+
}
|
|
302
|
+
function summarizeTelegramTransportError(error) {
|
|
303
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
304
|
+
const stderr = typeof error === "object" && error !== null && "stderr" in error
|
|
305
|
+
? String(error.stderr ?? "").trim()
|
|
306
|
+
: "";
|
|
307
|
+
const combined = [stderr, message].filter(Boolean).join(" ");
|
|
308
|
+
if (/Could not resolve host|Name or service not known|Temporary failure in name resolution/i.test(combined)) {
|
|
309
|
+
return "DNS lookup failed for api.telegram.org";
|
|
310
|
+
}
|
|
311
|
+
if (/timed out|Operation timed out|Connection timed out|code=28/i.test(combined)) {
|
|
312
|
+
return "connection to api.telegram.org timed out";
|
|
313
|
+
}
|
|
314
|
+
if (/SSL_ERROR_SYSCALL|SSL_read|tls/i.test(combined)) {
|
|
315
|
+
return "TLS connection to api.telegram.org failed";
|
|
316
|
+
}
|
|
317
|
+
return combined.split(/\r?\n/, 1)[0]?.slice(0, 300) || "unknown Telegram transport error";
|
|
318
|
+
}
|
|
319
|
+
function getRetryAfterMs(error) {
|
|
320
|
+
if (error instanceof TelegramPollingError && error.retryAfterMs !== undefined) {
|
|
321
|
+
return error.retryAfterMs;
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
async function writeTelegramTransportStatus(username, patch) {
|
|
326
|
+
if (!telegramTransportStatusPath) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const key = username || "unknown_bot";
|
|
330
|
+
telegramTransportStatuses[key] = {
|
|
331
|
+
...(telegramTransportStatuses[key] ?? {}),
|
|
332
|
+
...patch,
|
|
333
|
+
updatedAt: new Date().toISOString(),
|
|
334
|
+
};
|
|
335
|
+
const next = {
|
|
336
|
+
updatedAt: new Date().toISOString(),
|
|
337
|
+
bots: telegramTransportStatuses,
|
|
338
|
+
};
|
|
339
|
+
await fsp.mkdir(path.dirname(telegramTransportStatusPath), { recursive: true });
|
|
340
|
+
await fsp.writeFile(telegramTransportStatusPath, JSON.stringify(next, null, 2), "utf8");
|
|
341
|
+
}
|
|
342
|
+
function formatDuration(durationMs) {
|
|
343
|
+
const seconds = Math.max(1, Math.round(durationMs / 1000));
|
|
344
|
+
const minutes = Math.floor(seconds / 60);
|
|
345
|
+
const remainingSeconds = seconds % 60;
|
|
346
|
+
return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${seconds}s`;
|
|
347
|
+
}
|
|
348
|
+
async function getUpdatesViaCurl(token, offset) {
|
|
349
|
+
const url = new URL(`https://api.telegram.org/bot${token}/getUpdates`);
|
|
350
|
+
url.searchParams.set("timeout", String(TELEGRAM_GET_UPDATES_HTTP_TIMEOUT_SECONDS));
|
|
351
|
+
url.searchParams.set("limit", "50");
|
|
352
|
+
if (offset > 0) {
|
|
353
|
+
url.searchParams.set("offset", String(offset));
|
|
354
|
+
}
|
|
355
|
+
const { stdout, stderr } = await execFileAsync("curl", [
|
|
356
|
+
"-sS",
|
|
357
|
+
"-4",
|
|
358
|
+
"--max-time",
|
|
359
|
+
String(TELEGRAM_GET_UPDATES_CURL_TIMEOUT_SECONDS),
|
|
360
|
+
url.toString(),
|
|
361
|
+
]);
|
|
362
|
+
if (stderr?.trim()) {
|
|
363
|
+
console.error(`curl stderr for getUpdates: ${stderr.trim()}`);
|
|
364
|
+
}
|
|
365
|
+
const payload = JSON.parse(stdout);
|
|
366
|
+
if (!payload.ok || !Array.isArray(payload.result)) {
|
|
367
|
+
throw new TelegramPollingError(payload.description || "getUpdates returned an invalid payload.", typeof payload.parameters?.retry_after === "number"
|
|
368
|
+
? payload.parameters.retry_after * 1000
|
|
369
|
+
: undefined);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: payload.ok,
|
|
373
|
+
result: payload.result,
|
|
374
|
+
description: payload.description,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
class TelegramPollingError extends Error {
|
|
378
|
+
retryAfterMs;
|
|
379
|
+
constructor(message, retryAfterMs) {
|
|
380
|
+
super(message);
|
|
381
|
+
this.retryAfterMs = retryAfterMs;
|
|
382
|
+
this.name = "TelegramPollingError";
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
class AsyncSemaphore {
|
|
386
|
+
maxConcurrency;
|
|
387
|
+
active = 0;
|
|
388
|
+
waiters = [];
|
|
389
|
+
constructor(maxConcurrency) {
|
|
390
|
+
this.maxConcurrency = maxConcurrency;
|
|
391
|
+
}
|
|
392
|
+
async run(task) {
|
|
393
|
+
await this.acquire();
|
|
394
|
+
try {
|
|
395
|
+
return await task();
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
this.release();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async acquire() {
|
|
402
|
+
if (this.active < this.maxConcurrency) {
|
|
403
|
+
this.active += 1;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
await new Promise((resolve) => {
|
|
407
|
+
this.waiters.push(resolve);
|
|
408
|
+
});
|
|
409
|
+
this.active += 1;
|
|
410
|
+
}
|
|
411
|
+
release() {
|
|
412
|
+
this.active = Math.max(0, this.active - 1);
|
|
413
|
+
const next = this.waiters.shift();
|
|
414
|
+
if (next) {
|
|
415
|
+
next();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function buildBotInfo(token, index) {
|
|
420
|
+
const id = Number.parseInt(token.split(":", 1)[0] ?? "", 10);
|
|
421
|
+
const configuredUsername = config.telegramBotUsernames[index];
|
|
422
|
+
const fallbackUsername = knownBotUsername(id);
|
|
423
|
+
const username = configuredUsername || fallbackUsername || `bot_${Number.isFinite(id) ? id : index + 1}`;
|
|
424
|
+
return {
|
|
425
|
+
id: Number.isFinite(id) ? id : index + 1,
|
|
426
|
+
is_bot: true,
|
|
427
|
+
first_name: username,
|
|
428
|
+
username,
|
|
429
|
+
can_join_groups: false,
|
|
430
|
+
can_read_all_group_messages: false,
|
|
431
|
+
supports_inline_queries: false,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function knownBotUsername(id) {
|
|
435
|
+
if (id === 8369496408) {
|
|
436
|
+
return "codex_remoteagent_bot";
|
|
437
|
+
}
|
|
438
|
+
if (id === 8429712341) {
|
|
439
|
+
return "sqream_bot";
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
function commandExists(command) {
|
|
444
|
+
const trimmed = command.trim();
|
|
445
|
+
if (!trimmed) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
449
|
+
return fs.existsSync(trimmed);
|
|
450
|
+
}
|
|
451
|
+
if (process.platform === "win32") {
|
|
452
|
+
return spawnSync("where", [trimmed], { stdio: "ignore" }).status === 0;
|
|
453
|
+
}
|
|
454
|
+
return spawnSync("sh", ["-lc", 'command -v "$0" >/dev/null 2>&1', trimmed], { stdio: "ignore" }).status === 0;
|
|
455
|
+
}
|
|
456
|
+
async function acquireProcessLock(dataDir) {
|
|
457
|
+
await fsp.mkdir(dataDir, { recursive: true });
|
|
458
|
+
const lockPath = path.join(dataDir, "remoteagent.lock");
|
|
459
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
460
|
+
try {
|
|
461
|
+
const handle = await fsp.open(lockPath, "wx");
|
|
462
|
+
await handle.writeFile(String(process.pid));
|
|
463
|
+
await handle.close();
|
|
464
|
+
return lockPath;
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
const code = error.code;
|
|
468
|
+
if (code !== "EEXIST") {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const existingPid = Number.parseInt((await fsp.readFile(lockPath, "utf8").catch(() => "")).trim(), 10);
|
|
472
|
+
if (!Number.isFinite(existingPid) || !isProcessAlive(existingPid)) {
|
|
473
|
+
await fsp.rm(lockPath, { force: true }).catch(() => undefined);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`RemoteAgent is already running with PID ${existingPid}.`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
throw new Error("RemoteAgent could not acquire its process lock.");
|
|
480
|
+
}
|
|
481
|
+
function isProcessAlive(pid) {
|
|
482
|
+
try {
|
|
483
|
+
process.kill(pid, 0);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return error.code === "EPERM";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function registerProcessLifecycle() {
|
|
491
|
+
process.on("unhandledRejection", (reason) => {
|
|
492
|
+
console.error("Unhandled promise rejection:", reason);
|
|
493
|
+
});
|
|
494
|
+
process.on("uncaughtException", (error) => {
|
|
495
|
+
console.error("Uncaught exception:", error);
|
|
496
|
+
releaseProcessLockSync();
|
|
497
|
+
process.exit(1);
|
|
498
|
+
});
|
|
499
|
+
process.once("SIGINT", () => {
|
|
500
|
+
console.error("Received SIGINT, shutting down RemoteAgent.");
|
|
501
|
+
stopActiveProviderExecutionsForShutdown();
|
|
502
|
+
releaseProcessLockSync();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
});
|
|
505
|
+
process.once("SIGTERM", () => {
|
|
506
|
+
console.error("Received SIGTERM, shutting down RemoteAgent.");
|
|
507
|
+
stopActiveProviderExecutionsForShutdown();
|
|
508
|
+
releaseProcessLockSync();
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
process.once("exit", () => {
|
|
512
|
+
releaseProcessLockSync();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function stopActiveProviderExecutionsForShutdown() {
|
|
516
|
+
const stopped = terminateAllSpawnedExecutions();
|
|
517
|
+
if (stopped > 0) {
|
|
518
|
+
console.error(`Stopped ${stopped} active provider execution(s) during RemoteAgent shutdown.`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function releaseProcessLockSync() {
|
|
522
|
+
if (!processLockPath) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const recordedPid = fs.readFileSync(processLockPath, "utf8").trim();
|
|
527
|
+
if (recordedPid === String(process.pid)) {
|
|
528
|
+
fs.rmSync(processLockPath, { force: true });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Ignore best-effort cleanup errors.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { readSecretValue } from "./services/agent-memory-service.js";
|
|
6
|
+
function main() {
|
|
7
|
+
const [command, key] = process.argv.slice(2);
|
|
8
|
+
if (command !== "get" || !key) {
|
|
9
|
+
process.stderr.write("Usage: secret-helper get <KEY>\n");
|
|
10
|
+
process.exitCode = 2;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const dataDir = process.env.REMOTEAGENT_DATA_DIR?.trim()
|
|
14
|
+
|| process.env.DATA_DIR?.trim()
|
|
15
|
+
|| path.join(os.homedir(), ".remoteagent");
|
|
16
|
+
const value = readSecretValue(dataDir, key.trim());
|
|
17
|
+
if (!value) {
|
|
18
|
+
process.stderr.write(`Secret was not found: ${key}\n`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write(value);
|
|
23
|
+
}
|
|
24
|
+
main();
|