@todoforai/edge 0.12.14 → 0.12.16
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/index.js +99 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48418,8 +48418,12 @@ class BrowserExtensionBridge {
|
|
|
48418
48418
|
pending.ws.send(JSON.stringify({ type: "browser.command.result", requestId, error: "Browser bridge stopped" }));
|
|
48419
48419
|
}
|
|
48420
48420
|
this.pending.clear();
|
|
48421
|
+
if (isOpen(this.extensionWs))
|
|
48422
|
+
this.extensionWs.terminate();
|
|
48421
48423
|
this.extensionWs = null;
|
|
48424
|
+
this.wss?.clients.forEach((ws) => ws.terminate());
|
|
48422
48425
|
this.wss?.close();
|
|
48426
|
+
this.server?.closeAllConnections();
|
|
48423
48427
|
this.server?.close();
|
|
48424
48428
|
this.wss = undefined;
|
|
48425
48429
|
this.server = undefined;
|
|
@@ -48608,7 +48612,7 @@ import path4 from "path";
|
|
|
48608
48612
|
import fs3 from "fs";
|
|
48609
48613
|
import os3 from "os";
|
|
48610
48614
|
import path3 from "path";
|
|
48611
|
-
import { execSync, spawnSync } from "child_process";
|
|
48615
|
+
import { execSync, spawnSync, execFile } from "child_process";
|
|
48612
48616
|
|
|
48613
48617
|
// src/tool-catalog.ts
|
|
48614
48618
|
import os2 from "os";
|
|
@@ -49106,7 +49110,7 @@ function buildEnvWithTools() {
|
|
|
49106
49110
|
}
|
|
49107
49111
|
function whichWithTools(name) {
|
|
49108
49112
|
const dirs = [...toolPathEntries(), ...(process.env.PATH || "").split(path3.delimiter)];
|
|
49109
|
-
const exts = os3.platform() === "win32" ? ["
|
|
49113
|
+
const exts = os3.platform() === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
49110
49114
|
for (const dir of dirs) {
|
|
49111
49115
|
for (const ext of exts) {
|
|
49112
49116
|
const full = path3.join(dir, name + ext);
|
|
@@ -49214,12 +49218,18 @@ function findFileRecursive(dir, names) {
|
|
|
49214
49218
|
return null;
|
|
49215
49219
|
}
|
|
49216
49220
|
function installWithNpm(name, pkg) {
|
|
49217
|
-
const npm = whichWithTools("npm") || "npm";
|
|
49218
49221
|
log2("info", `Installing ${name} via npm (${pkg})`);
|
|
49219
|
-
const result = spawnSync(npm, ["install", "--prefix", TOOLS_DIR, pkg], {
|
|
49222
|
+
const result = spawnSync("npm", ["install", "--prefix", TOOLS_DIR, pkg], {
|
|
49220
49223
|
stdio: "pipe",
|
|
49221
|
-
timeout: 120000
|
|
49224
|
+
timeout: 120000,
|
|
49225
|
+
shell: true
|
|
49222
49226
|
});
|
|
49227
|
+
if (result.error) {
|
|
49228
|
+
throw new Error(`npm install failed: ${result.error.message}`);
|
|
49229
|
+
}
|
|
49230
|
+
if (result.signal) {
|
|
49231
|
+
throw new Error(`npm install killed by ${result.signal}${result.signal === "SIGTERM" ? " (likely timed out after 120s)" : ""}`);
|
|
49232
|
+
}
|
|
49223
49233
|
if (result.status !== 0) {
|
|
49224
49234
|
throw new Error(`npm install failed: ${result.stderr?.toString() || result.stdout?.toString() || `exit code ${result.status}`}`);
|
|
49225
49235
|
}
|
|
@@ -49278,7 +49288,9 @@ function installWithPip(name, pkg) {
|
|
|
49278
49288
|
log2("info", `Installing ${name} via pip (${pkg})`);
|
|
49279
49289
|
const args = useVenv ? ["-m", "pip", "install", pkg] : ["-m", "pip", "install", "--user", pkg];
|
|
49280
49290
|
const result = spawnSync(python, args, { stdio: "pipe", timeout: 120000 });
|
|
49281
|
-
if (result.
|
|
49291
|
+
if (result.signal) {
|
|
49292
|
+
log2("error", `Failed to install ${name}: killed by ${result.signal}${result.signal === "SIGTERM" ? " (likely timed out after 120s)" : ""}`);
|
|
49293
|
+
} else if (result.status !== 0) {
|
|
49282
49294
|
log2("error", `Failed to install ${name}: ${result.stderr?.toString() || result.stdout?.toString()}`);
|
|
49283
49295
|
}
|
|
49284
49296
|
}
|
|
@@ -49330,8 +49342,7 @@ function uninstallTool(name) {
|
|
|
49330
49342
|
fs3.unlinkSync(p);
|
|
49331
49343
|
}
|
|
49332
49344
|
} else if (installerType === "npm") {
|
|
49333
|
-
|
|
49334
|
-
spawnSync(npm, ["uninstall", "--prefix", TOOLS_DIR, pkg], { stdio: "pipe", timeout: 30000 });
|
|
49345
|
+
spawnSync("npm", ["uninstall", "--prefix", TOOLS_DIR, pkg], { stdio: "pipe", timeout: 30000, shell: true });
|
|
49335
49346
|
} else if (installerType === "pip") {
|
|
49336
49347
|
const venvPython = os3.platform() === "win32" ? path3.join(TOOLS_DIR, "venv", "Scripts", "python.exe") : path3.join(TOOLS_DIR, "venv", "bin", "python");
|
|
49337
49348
|
const python = fs3.existsSync(venvPython) ? venvPython : "python3";
|
|
@@ -49344,27 +49355,39 @@ function uninstallTool(name) {
|
|
|
49344
49355
|
return false;
|
|
49345
49356
|
}
|
|
49346
49357
|
}
|
|
49347
|
-
function
|
|
49358
|
+
function execShellAsync(cmd, env, timeout) {
|
|
49359
|
+
return new Promise((resolve) => {
|
|
49360
|
+
execFile("sh", ["-c", cmd], { env, timeout, encoding: "utf-8", maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
49361
|
+
resolve({ status: err ? 1 : 0, stdout: (stdout || "").toString(), stderr: (stderr || "").toString() });
|
|
49362
|
+
});
|
|
49363
|
+
});
|
|
49364
|
+
}
|
|
49365
|
+
async function scanCatalogTools() {
|
|
49348
49366
|
const result = {};
|
|
49349
49367
|
const env = buildEnvWithTools();
|
|
49350
|
-
|
|
49368
|
+
const entries = Object.entries(TOOL_CATALOG);
|
|
49369
|
+
const installed = [];
|
|
49370
|
+
for (const [name, entry] of entries) {
|
|
49351
49371
|
if (!isToolInstalled(name)) {
|
|
49352
49372
|
result[name] = { installed: false };
|
|
49353
|
-
|
|
49373
|
+
} else {
|
|
49374
|
+
installed.push([name, entry]);
|
|
49354
49375
|
}
|
|
49376
|
+
}
|
|
49377
|
+
await Promise.all(installed.map(async ([name, entry]) => {
|
|
49355
49378
|
const state = { installed: true };
|
|
49356
49379
|
if (entry.versionCmd) {
|
|
49357
49380
|
try {
|
|
49358
|
-
const r =
|
|
49381
|
+
const r = await execShellAsync(entry.versionCmd, env, 5000);
|
|
49359
49382
|
if (r.status === 0)
|
|
49360
|
-
state.version =
|
|
49383
|
+
state.version = r.stdout.trim().slice(0, 100);
|
|
49361
49384
|
} catch {}
|
|
49362
49385
|
}
|
|
49363
49386
|
if (entry.statusCmd) {
|
|
49364
49387
|
try {
|
|
49365
|
-
const r =
|
|
49388
|
+
const r = await execShellAsync(entry.statusCmd, env, 1e4);
|
|
49366
49389
|
state.authenticated = r.status === 0;
|
|
49367
|
-
state.statusOutput = (r.stdout || r.stderr
|
|
49390
|
+
state.statusOutput = (r.stdout || r.stderr).trim().slice(0, 200);
|
|
49368
49391
|
} catch {
|
|
49369
49392
|
state.authenticated = false;
|
|
49370
49393
|
}
|
|
@@ -49372,7 +49395,7 @@ function scanCatalogTools() {
|
|
|
49372
49395
|
state.authenticated = true;
|
|
49373
49396
|
}
|
|
49374
49397
|
result[name] = state;
|
|
49375
|
-
}
|
|
49398
|
+
}));
|
|
49376
49399
|
return result;
|
|
49377
49400
|
}
|
|
49378
49401
|
var MOUNT_FLAGS = [
|
|
@@ -51272,6 +51295,9 @@ function waitForCompletion(blockId, timeoutMs) {
|
|
|
51272
51295
|
function getBlockOutput(blockId) {
|
|
51273
51296
|
return outputBuffers.get(blockId)?.getOutput() ?? "";
|
|
51274
51297
|
}
|
|
51298
|
+
function getBlockRawOutput(blockId) {
|
|
51299
|
+
return outputBuffers.get(blockId)?.getRawIfComplete() ?? null;
|
|
51300
|
+
}
|
|
51275
51301
|
function clearBlockOutput(blockId) {
|
|
51276
51302
|
outputBuffers.delete(blockId);
|
|
51277
51303
|
}
|
|
@@ -51327,7 +51353,7 @@ register("install_tool", async (args) => {
|
|
|
51327
51353
|
if (!name || !(name in TOOL_CATALOG)) {
|
|
51328
51354
|
return { success: false, error: `Unknown tool: ${name}` };
|
|
51329
51355
|
}
|
|
51330
|
-
const scan = scanCatalogTools();
|
|
51356
|
+
const scan = await scanCatalogTools();
|
|
51331
51357
|
if (scan[name]?.installed) {
|
|
51332
51358
|
return { success: true, alreadyInstalled: true, tool: name };
|
|
51333
51359
|
}
|
|
@@ -51337,7 +51363,7 @@ register("install_tool", async (args) => {
|
|
|
51337
51363
|
}
|
|
51338
51364
|
const edge = getGlobalEdgeInstance();
|
|
51339
51365
|
if (edge) {
|
|
51340
|
-
await edge.updateConfig({ installedTools: scanCatalogTools() });
|
|
51366
|
+
await edge.updateConfig({ installedTools: await scanCatalogTools() });
|
|
51341
51367
|
}
|
|
51342
51368
|
return { success: true, tool: name, label: TOOL_CATALOG[name].label };
|
|
51343
51369
|
});
|
|
@@ -51350,7 +51376,7 @@ register("uninstall_tool", async (args) => {
|
|
|
51350
51376
|
if (success) {
|
|
51351
51377
|
const edge = getGlobalEdgeInstance();
|
|
51352
51378
|
if (edge)
|
|
51353
|
-
await edge.updateConfig({ installedTools: scanCatalogTools() });
|
|
51379
|
+
await edge.updateConfig({ installedTools: await scanCatalogTools() });
|
|
51354
51380
|
}
|
|
51355
51381
|
return { success, tool: name };
|
|
51356
51382
|
});
|
|
@@ -51486,6 +51512,19 @@ function extractTrailingTail(cmd) {
|
|
|
51486
51512
|
`).slice(-n).join(`
|
|
51487
51513
|
`) };
|
|
51488
51514
|
}
|
|
51515
|
+
var DATA_URL_IMAGE_REGEX = /^data:(image\/[^;]+);base64,[A-Za-z0-9+/]+=*$/;
|
|
51516
|
+
function detectContentType(output, cmd) {
|
|
51517
|
+
const trimmed = output.trim();
|
|
51518
|
+
const match = trimmed.match(DATA_URL_IMAGE_REGEX);
|
|
51519
|
+
if (match) {
|
|
51520
|
+
console.log(`
|
|
51521
|
+
\uD83D\uDDBC️ [edge] Image output detected! type=${match[1]} size=${trimmed.length} chars${cmd ? `
|
|
51522
|
+
cmd: ${cmd}` : ""}
|
|
51523
|
+
`);
|
|
51524
|
+
return { result: trimmed, contentType: match[1] };
|
|
51525
|
+
}
|
|
51526
|
+
return { result: output };
|
|
51527
|
+
}
|
|
51489
51528
|
register("execute_shell_command", async (args, client) => {
|
|
51490
51529
|
const { cmd, timeout = 120, root_path = "", todoId = "", messageId = "", blockId = "" } = args;
|
|
51491
51530
|
const canStream = !!(todoId && blockId && client);
|
|
@@ -51496,7 +51535,7 @@ register("execute_shell_command", async (args, client) => {
|
|
|
51496
51535
|
resolve((stdout || "") + (stderr || ""));
|
|
51497
51536
|
});
|
|
51498
51537
|
});
|
|
51499
|
-
return { cmd, result };
|
|
51538
|
+
return { cmd, ...detectContentType(result, cmd) };
|
|
51500
51539
|
}
|
|
51501
51540
|
const { execCmd, postFilter } = extractTrailingTail(cmd);
|
|
51502
51541
|
try {
|
|
@@ -51507,11 +51546,12 @@ register("execute_shell_command", async (args, client) => {
|
|
|
51507
51546
|
return { __awaiting_approval__: true };
|
|
51508
51547
|
}
|
|
51509
51548
|
await waitForCompletion(blockId, (timeout + 5) * 1000);
|
|
51510
|
-
|
|
51549
|
+
const rawOutput = getBlockRawOutput(blockId);
|
|
51550
|
+
let output = rawOutput ?? getBlockOutput(blockId);
|
|
51511
51551
|
clearBlockOutput(blockId);
|
|
51512
51552
|
if (postFilter)
|
|
51513
51553
|
output = postFilter(output);
|
|
51514
|
-
return { cmd, result: output };
|
|
51554
|
+
return rawOutput !== null ? { cmd, ...detectContentType(output, cmd) } : { cmd, result: output };
|
|
51515
51555
|
} catch (e) {
|
|
51516
51556
|
throw e;
|
|
51517
51557
|
} finally {
|
|
@@ -51939,6 +51979,8 @@ class TODOforAIEdge {
|
|
|
51939
51979
|
addWorkspacePath;
|
|
51940
51980
|
frontendWs = null;
|
|
51941
51981
|
browserExtensionBridge;
|
|
51982
|
+
stopping = false;
|
|
51983
|
+
reconnectTimer;
|
|
51942
51984
|
edgeConfig = {
|
|
51943
51985
|
id: "",
|
|
51944
51986
|
name: "Name uninitialized",
|
|
@@ -52117,7 +52159,7 @@ class TODOforAIEdge {
|
|
|
52117
52159
|
id2 += String.fromCharCode(frame[i10]);
|
|
52118
52160
|
const data = frame.slice(36);
|
|
52119
52161
|
this.pendingBinaries.set(id2, data);
|
|
52120
|
-
setTimeout(() => this.pendingBinaries.delete(id2), 60000);
|
|
52162
|
+
setTimeout(() => this.pendingBinaries.delete(id2), 60000).unref();
|
|
52121
52163
|
}
|
|
52122
52164
|
async handleMessage(raw) {
|
|
52123
52165
|
let data;
|
|
@@ -52149,7 +52191,7 @@ class TODOforAIEdge {
|
|
|
52149
52191
|
this.edgeConfig.id = this.edgeId;
|
|
52150
52192
|
console.log(`\x1B[32m\x1B[1m\uD83D\uDD17 Connected edge=${this.edgeId} user=${this.userId}\x1B[0m`);
|
|
52151
52193
|
run(async () => {
|
|
52152
|
-
this.updateConfig({ installedTools: scanCatalogTools() });
|
|
52194
|
+
this.updateConfig({ installedTools: await scanCatalogTools() });
|
|
52153
52195
|
autoMountRcloneRemotes();
|
|
52154
52196
|
});
|
|
52155
52197
|
break;
|
|
@@ -52268,6 +52310,10 @@ class TODOforAIEdge {
|
|
|
52268
52310
|
console.log(`[info] WebSocket closed code=${code} clean=${clean} reason=${reasonText}`);
|
|
52269
52311
|
if (code === 4001) {
|
|
52270
52312
|
console.log(`\x1B[33m[info] ${reasonText}. Not reconnecting.\x1B[0m`);
|
|
52313
|
+
console.log(`\x1B[33m[info] To replace the existing connection, restart with: todoforai-edge --kill\x1B[0m`);
|
|
52314
|
+
reject(new ServerError(reasonText));
|
|
52315
|
+
} else if (code === 4002) {
|
|
52316
|
+
console.log(`\x1B[33m[info] ${reasonText}. This instance was replaced by a new connection.\x1B[0m`);
|
|
52271
52317
|
reject(new ServerError(reasonText));
|
|
52272
52318
|
} else {
|
|
52273
52319
|
resolve();
|
|
@@ -52289,10 +52335,12 @@ class TODOforAIEdge {
|
|
|
52289
52335
|
console.log(`\x1B[36m\x1B[1m\uD83D\uDC46 Fingerprint:\x1B[0m ${this.fingerprint}`);
|
|
52290
52336
|
const maxAttempts = 20;
|
|
52291
52337
|
let attempt = 0;
|
|
52292
|
-
while (attempt < maxAttempts) {
|
|
52338
|
+
while (attempt < maxAttempts && !this.stopping) {
|
|
52293
52339
|
console.log(`[info] Connecting (attempt ${attempt + 1}/${maxAttempts})`);
|
|
52294
52340
|
try {
|
|
52295
52341
|
await this.connect();
|
|
52342
|
+
if (this.stopping)
|
|
52343
|
+
break;
|
|
52296
52344
|
attempt = 0;
|
|
52297
52345
|
} catch (e) {
|
|
52298
52346
|
if (e instanceof AuthenticationError) {
|
|
@@ -52309,16 +52357,26 @@ class TODOforAIEdge {
|
|
|
52309
52357
|
this.connected = false;
|
|
52310
52358
|
this.ws = null;
|
|
52311
52359
|
}
|
|
52312
|
-
if (attempt > 0 && attempt < maxAttempts) {
|
|
52360
|
+
if (attempt > 0 && attempt < maxAttempts && !this.stopping) {
|
|
52313
52361
|
const delay = Math.min(4 + attempt, 20);
|
|
52314
52362
|
console.log(`[info] Reconnecting in ${delay}s...`);
|
|
52315
|
-
await new Promise((r) =>
|
|
52363
|
+
await new Promise((r) => {
|
|
52364
|
+
this.reconnectTimer = setTimeout(r, delay * 1000);
|
|
52365
|
+
});
|
|
52316
52366
|
}
|
|
52317
52367
|
}
|
|
52318
52368
|
if (attempt >= maxAttempts) {
|
|
52319
52369
|
console.error("\x1B[31mMax reconnection attempts reached.\x1B[0m");
|
|
52320
52370
|
}
|
|
52321
52371
|
}
|
|
52372
|
+
stop() {
|
|
52373
|
+
this.stopping = true;
|
|
52374
|
+
clearTimeout(this.reconnectTimer);
|
|
52375
|
+
this.stopHeartbeat();
|
|
52376
|
+
this.browserExtensionBridge.stop();
|
|
52377
|
+
this.frontendWs?.close();
|
|
52378
|
+
this.ws?.terminate();
|
|
52379
|
+
}
|
|
52322
52380
|
async getFrontendWs() {
|
|
52323
52381
|
if (!this.frontendWs || !this.frontendWs.connected) {
|
|
52324
52382
|
this.frontendWs = new FrontendWebSocket(this.api.apiUrl, this.api.apiKey);
|
|
@@ -52426,16 +52484,27 @@ async function main() {
|
|
|
52426
52484
|
await edge.ensureApiKey(true);
|
|
52427
52485
|
const lp2 = lockPath(config.apiUrl, edge.userId);
|
|
52428
52486
|
if (!acquireLock(lp2, config.kill)) {
|
|
52429
|
-
console.error(
|
|
52487
|
+
console.error(`\x1B[31mAnother edge is already running for this user+server. Use --kill to replace it, or delete the lock file: ${lp2}\x1B[0m`);
|
|
52430
52488
|
process.exit(1);
|
|
52431
52489
|
}
|
|
52490
|
+
let cleaned = false;
|
|
52432
52491
|
const cleanup = () => {
|
|
52492
|
+
if (cleaned)
|
|
52493
|
+
return;
|
|
52494
|
+
cleaned = true;
|
|
52495
|
+
edge.stop();
|
|
52433
52496
|
unmountAllRclone();
|
|
52434
52497
|
releaseLock(lp2);
|
|
52435
52498
|
};
|
|
52436
52499
|
process.on("exit", cleanup);
|
|
52437
|
-
process.on("SIGINT", () =>
|
|
52438
|
-
|
|
52500
|
+
process.on("SIGINT", () => {
|
|
52501
|
+
cleanup();
|
|
52502
|
+
process.exit(0);
|
|
52503
|
+
});
|
|
52504
|
+
process.on("SIGTERM", () => {
|
|
52505
|
+
cleanup();
|
|
52506
|
+
process.exit(0);
|
|
52507
|
+
});
|
|
52439
52508
|
await edge.start();
|
|
52440
52509
|
}
|
|
52441
52510
|
main().catch((e) => {
|