@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.
Files changed (2) hide show
  1. package/dist/index.js +99 -30
  2. 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" ? ["", ".exe", ".cmd", ".bat"] : [""];
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.status !== 0) {
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
- const npm = whichWithTools("npm") || "npm";
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 scanCatalogTools() {
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
- for (const [name, entry] of Object.entries(TOOL_CATALOG)) {
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
- continue;
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 = spawnSync("sh", ["-c", entry.versionCmd], { env, timeout: 5000, encoding: "utf-8" });
49381
+ const r = await execShellAsync(entry.versionCmd, env, 5000);
49359
49382
  if (r.status === 0)
49360
- state.version = (r.stdout || "").toString().trim().slice(0, 100);
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 = spawnSync("sh", ["-c", entry.statusCmd], { env, timeout: 1e4, encoding: "utf-8" });
49388
+ const r = await execShellAsync(entry.statusCmd, env, 1e4);
49366
49389
  state.authenticated = r.status === 0;
49367
- state.statusOutput = (r.stdout || r.stderr || "").toString().trim().slice(0, 200);
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
- let output = getBlockOutput(blockId);
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) => setTimeout(r, delay * 1000));
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("\x1B[31mAnother edge is already running for this user+server. Use --kill to replace it.\x1B[0m");
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", () => process.exit(0));
52438
- process.on("SIGTERM", () => process.exit(0));
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.12.14",
3
+ "version": "0.12.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"