@todoforai/edge 0.12.13 → 0.12.15

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 +136 -47
  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";
@@ -49344,27 +49348,39 @@ function uninstallTool(name) {
49344
49348
  return false;
49345
49349
  }
49346
49350
  }
49347
- function scanCatalogTools() {
49351
+ function execShellAsync(cmd, env, timeout) {
49352
+ return new Promise((resolve) => {
49353
+ execFile("sh", ["-c", cmd], { env, timeout, encoding: "utf-8", maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
49354
+ resolve({ status: err ? 1 : 0, stdout: (stdout || "").toString(), stderr: (stderr || "").toString() });
49355
+ });
49356
+ });
49357
+ }
49358
+ async function scanCatalogTools() {
49348
49359
  const result = {};
49349
49360
  const env = buildEnvWithTools();
49350
- for (const [name, entry] of Object.entries(TOOL_CATALOG)) {
49361
+ const entries = Object.entries(TOOL_CATALOG);
49362
+ const installed = [];
49363
+ for (const [name, entry] of entries) {
49351
49364
  if (!isToolInstalled(name)) {
49352
49365
  result[name] = { installed: false };
49353
- continue;
49366
+ } else {
49367
+ installed.push([name, entry]);
49354
49368
  }
49369
+ }
49370
+ await Promise.all(installed.map(async ([name, entry]) => {
49355
49371
  const state = { installed: true };
49356
49372
  if (entry.versionCmd) {
49357
49373
  try {
49358
- const r = spawnSync("sh", ["-c", entry.versionCmd], { env, timeout: 5000, encoding: "utf-8" });
49374
+ const r = await execShellAsync(entry.versionCmd, env, 5000);
49359
49375
  if (r.status === 0)
49360
- state.version = (r.stdout || "").toString().trim().slice(0, 100);
49376
+ state.version = r.stdout.trim().slice(0, 100);
49361
49377
  } catch {}
49362
49378
  }
49363
49379
  if (entry.statusCmd) {
49364
49380
  try {
49365
- const r = spawnSync("sh", ["-c", entry.statusCmd], { env, timeout: 1e4, encoding: "utf-8" });
49381
+ const r = await execShellAsync(entry.statusCmd, env, 1e4);
49366
49382
  state.authenticated = r.status === 0;
49367
- state.statusOutput = (r.stdout || r.stderr || "").toString().trim().slice(0, 200);
49383
+ state.statusOutput = (r.stdout || r.stderr).trim().slice(0, 200);
49368
49384
  } catch {
49369
49385
  state.authenticated = false;
49370
49386
  }
@@ -49372,7 +49388,7 @@ function scanCatalogTools() {
49372
49388
  state.authenticated = true;
49373
49389
  }
49374
49390
  result[name] = state;
49375
- }
49391
+ }));
49376
49392
  return result;
49377
49393
  }
49378
49394
  var MOUNT_FLAGS = [
@@ -50906,8 +50922,10 @@ async function readFileContent(filePath, rootPath, fallbackRootPaths) {
50906
50922
  import os5 from "os";
50907
50923
  import fs8 from "fs";
50908
50924
  import path5 from "path";
50925
+ import { spawn as nodeSpawn } from "child_process";
50909
50926
  var IS_WIN = os5.platform() === "win32";
50910
- var HAS_BUN_TERMINAL = typeof Bun.Terminal === "function";
50927
+ var HAS_BUN = typeof globalThis.Bun !== "undefined";
50928
+ var HAS_BUN_TERMINAL = HAS_BUN && typeof Bun.Terminal === "function";
50911
50929
  function whichSync(name) {
50912
50930
  const dirs = (process.env.PATH || "").split(path5.delimiter);
50913
50931
  const exts = IS_WIN ? ["", ".exe", ".cmd", ".bat"] : [""];
@@ -51011,6 +51029,13 @@ ${this.lastPart}`;
51011
51029
  }
51012
51030
  const all = current ? [...this.savedSegments, current] : [...this.savedSegments];
51013
51031
  return all.join(`
51032
+ `);
51033
+ }
51034
+ getRawIfComplete() {
51035
+ if (this.truncated)
51036
+ return null;
51037
+ const all = this.firstPart ? [...this.savedSegments, this.firstPart] : [...this.savedSegments];
51038
+ return all.join(`
51014
51039
  `);
51015
51040
  }
51016
51041
  }
@@ -51134,29 +51159,51 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51134
51159
  });
51135
51160
  };
51136
51161
  const spawnWithPipes = () => {
51137
- const proc = Bun.spawn([sc2.shell, ...sc2.args], {
51138
- cwd,
51139
- env,
51140
- stdin: "pipe",
51141
- stdout: "pipe",
51142
- stderr: "pipe"
51143
- });
51144
- const handle = { proc, pid: proc.pid };
51145
- processes.set(blockId, handle);
51146
- const timer = startTimeout();
51147
- const pipeStream = async (stream) => {
51148
- if (!stream)
51149
- return;
51150
- const decoder = new TextDecoder;
51151
- for await (const chunk of stream) {
51152
- await onData(decoder.decode(chunk, { stream: true }));
51153
- }
51154
- };
51155
- Promise.all([
51156
- pipeStream(proc.stdout),
51157
- pipeStream(proc.stderr),
51158
- proc.exited
51159
- ]).then(([, , code]) => onExit(code ?? -1, timer)).catch(() => onExit(-1, timer));
51162
+ if (HAS_BUN) {
51163
+ const proc = Bun.spawn([sc2.shell, ...sc2.args], {
51164
+ cwd,
51165
+ env,
51166
+ stdin: "pipe",
51167
+ stdout: "pipe",
51168
+ stderr: "pipe"
51169
+ });
51170
+ const handle = { proc, pid: proc.pid };
51171
+ processes.set(blockId, handle);
51172
+ const timer = startTimeout();
51173
+ const pipeStream = async (stream) => {
51174
+ if (!stream)
51175
+ return;
51176
+ const decoder = new TextDecoder;
51177
+ for await (const chunk of stream) {
51178
+ await onData(decoder.decode(chunk, { stream: true }));
51179
+ }
51180
+ };
51181
+ Promise.all([
51182
+ pipeStream(proc.stdout),
51183
+ pipeStream(proc.stderr),
51184
+ proc.exited
51185
+ ]).then(([, , code]) => onExit(code ?? -1, timer)).catch(() => onExit(-1, timer));
51186
+ } else {
51187
+ const proc = nodeSpawn(sc2.shell, sc2.args, {
51188
+ cwd,
51189
+ env,
51190
+ stdio: ["pipe", "pipe", "pipe"]
51191
+ });
51192
+ const handle = { proc, pid: proc.pid ?? -1 };
51193
+ processes.set(blockId, handle);
51194
+ const timer = startTimeout();
51195
+ let exited = false;
51196
+ const exit = (code) => {
51197
+ if (!exited) {
51198
+ exited = true;
51199
+ onExit(code, timer);
51200
+ }
51201
+ };
51202
+ proc.stdout?.on("data", (chunk) => onData(chunk.toString()));
51203
+ proc.stderr?.on("data", (chunk) => onData(chunk.toString()));
51204
+ proc.on("close", (code) => exit(code ?? -1));
51205
+ proc.on("error", () => exit(-1));
51206
+ }
51160
51207
  };
51161
51208
  if (HAS_BUN_TERMINAL) {
51162
51209
  try {
@@ -51241,6 +51288,9 @@ function waitForCompletion(blockId, timeoutMs) {
51241
51288
  function getBlockOutput(blockId) {
51242
51289
  return outputBuffers.get(blockId)?.getOutput() ?? "";
51243
51290
  }
51291
+ function getBlockRawOutput(blockId) {
51292
+ return outputBuffers.get(blockId)?.getRawIfComplete() ?? null;
51293
+ }
51244
51294
  function clearBlockOutput(blockId) {
51245
51295
  outputBuffers.delete(blockId);
51246
51296
  }
@@ -51296,7 +51346,7 @@ register("install_tool", async (args) => {
51296
51346
  if (!name || !(name in TOOL_CATALOG)) {
51297
51347
  return { success: false, error: `Unknown tool: ${name}` };
51298
51348
  }
51299
- const scan = scanCatalogTools();
51349
+ const scan = await scanCatalogTools();
51300
51350
  if (scan[name]?.installed) {
51301
51351
  return { success: true, alreadyInstalled: true, tool: name };
51302
51352
  }
@@ -51306,7 +51356,7 @@ register("install_tool", async (args) => {
51306
51356
  }
51307
51357
  const edge = getGlobalEdgeInstance();
51308
51358
  if (edge) {
51309
- await edge.updateConfig({ installedTools: scanCatalogTools() });
51359
+ await edge.updateConfig({ installedTools: await scanCatalogTools() });
51310
51360
  }
51311
51361
  return { success: true, tool: name, label: TOOL_CATALOG[name].label };
51312
51362
  });
@@ -51319,7 +51369,7 @@ register("uninstall_tool", async (args) => {
51319
51369
  if (success) {
51320
51370
  const edge = getGlobalEdgeInstance();
51321
51371
  if (edge)
51322
- await edge.updateConfig({ installedTools: scanCatalogTools() });
51372
+ await edge.updateConfig({ installedTools: await scanCatalogTools() });
51323
51373
  }
51324
51374
  return { success, tool: name };
51325
51375
  });
@@ -51455,6 +51505,19 @@ function extractTrailingTail(cmd) {
51455
51505
  `).slice(-n).join(`
51456
51506
  `) };
51457
51507
  }
51508
+ var DATA_URL_IMAGE_REGEX = /^data:(image\/[^;]+);base64,[A-Za-z0-9+/]+=*$/;
51509
+ function detectContentType(output, cmd) {
51510
+ const trimmed = output.trim();
51511
+ const match = trimmed.match(DATA_URL_IMAGE_REGEX);
51512
+ if (match) {
51513
+ console.log(`
51514
+ \uD83D\uDDBC️ [edge] Image output detected! type=${match[1]} size=${trimmed.length} chars${cmd ? `
51515
+ cmd: ${cmd}` : ""}
51516
+ `);
51517
+ return { result: trimmed, contentType: match[1] };
51518
+ }
51519
+ return { result: output };
51520
+ }
51458
51521
  register("execute_shell_command", async (args, client) => {
51459
51522
  const { cmd, timeout = 120, root_path = "", todoId = "", messageId = "", blockId = "" } = args;
51460
51523
  const canStream = !!(todoId && blockId && client);
@@ -51465,7 +51528,7 @@ register("execute_shell_command", async (args, client) => {
51465
51528
  resolve((stdout || "") + (stderr || ""));
51466
51529
  });
51467
51530
  });
51468
- return { cmd, result };
51531
+ return { cmd, ...detectContentType(result, cmd) };
51469
51532
  }
51470
51533
  const { execCmd, postFilter } = extractTrailingTail(cmd);
51471
51534
  try {
@@ -51476,11 +51539,12 @@ register("execute_shell_command", async (args, client) => {
51476
51539
  return { __awaiting_approval__: true };
51477
51540
  }
51478
51541
  await waitForCompletion(blockId, (timeout + 5) * 1000);
51479
- let output = getBlockOutput(blockId);
51542
+ const rawOutput = getBlockRawOutput(blockId);
51543
+ let output = rawOutput ?? getBlockOutput(blockId);
51480
51544
  clearBlockOutput(blockId);
51481
51545
  if (postFilter)
51482
51546
  output = postFilter(output);
51483
- return { cmd, result: output };
51547
+ return rawOutput !== null ? { cmd, ...detectContentType(output, cmd) } : { cmd, result: output };
51484
51548
  } catch (e) {
51485
51549
  throw e;
51486
51550
  } finally {
@@ -51908,6 +51972,8 @@ class TODOforAIEdge {
51908
51972
  addWorkspacePath;
51909
51973
  frontendWs = null;
51910
51974
  browserExtensionBridge;
51975
+ stopping = false;
51976
+ reconnectTimer;
51911
51977
  edgeConfig = {
51912
51978
  id: "",
51913
51979
  name: "Name uninitialized",
@@ -52086,7 +52152,7 @@ class TODOforAIEdge {
52086
52152
  id2 += String.fromCharCode(frame[i10]);
52087
52153
  const data = frame.slice(36);
52088
52154
  this.pendingBinaries.set(id2, data);
52089
- setTimeout(() => this.pendingBinaries.delete(id2), 60000);
52155
+ setTimeout(() => this.pendingBinaries.delete(id2), 60000).unref();
52090
52156
  }
52091
52157
  async handleMessage(raw) {
52092
52158
  let data;
@@ -52118,7 +52184,7 @@ class TODOforAIEdge {
52118
52184
  this.edgeConfig.id = this.edgeId;
52119
52185
  console.log(`\x1B[32m\x1B[1m\uD83D\uDD17 Connected edge=${this.edgeId} user=${this.userId}\x1B[0m`);
52120
52186
  run(async () => {
52121
- this.updateConfig({ installedTools: scanCatalogTools() });
52187
+ this.updateConfig({ installedTools: await scanCatalogTools() });
52122
52188
  autoMountRcloneRemotes();
52123
52189
  });
52124
52190
  break;
@@ -52258,10 +52324,12 @@ class TODOforAIEdge {
52258
52324
  console.log(`\x1B[36m\x1B[1m\uD83D\uDC46 Fingerprint:\x1B[0m ${this.fingerprint}`);
52259
52325
  const maxAttempts = 20;
52260
52326
  let attempt = 0;
52261
- while (attempt < maxAttempts) {
52327
+ while (attempt < maxAttempts && !this.stopping) {
52262
52328
  console.log(`[info] Connecting (attempt ${attempt + 1}/${maxAttempts})`);
52263
52329
  try {
52264
52330
  await this.connect();
52331
+ if (this.stopping)
52332
+ break;
52265
52333
  attempt = 0;
52266
52334
  } catch (e) {
52267
52335
  if (e instanceof AuthenticationError) {
@@ -52278,16 +52346,26 @@ class TODOforAIEdge {
52278
52346
  this.connected = false;
52279
52347
  this.ws = null;
52280
52348
  }
52281
- if (attempt > 0 && attempt < maxAttempts) {
52349
+ if (attempt > 0 && attempt < maxAttempts && !this.stopping) {
52282
52350
  const delay = Math.min(4 + attempt, 20);
52283
52351
  console.log(`[info] Reconnecting in ${delay}s...`);
52284
- await new Promise((r) => setTimeout(r, delay * 1000));
52352
+ await new Promise((r) => {
52353
+ this.reconnectTimer = setTimeout(r, delay * 1000);
52354
+ });
52285
52355
  }
52286
52356
  }
52287
52357
  if (attempt >= maxAttempts) {
52288
52358
  console.error("\x1B[31mMax reconnection attempts reached.\x1B[0m");
52289
52359
  }
52290
52360
  }
52361
+ stop() {
52362
+ this.stopping = true;
52363
+ clearTimeout(this.reconnectTimer);
52364
+ this.stopHeartbeat();
52365
+ this.browserExtensionBridge.stop();
52366
+ this.frontendWs?.close();
52367
+ this.ws?.terminate();
52368
+ }
52291
52369
  async getFrontendWs() {
52292
52370
  if (!this.frontendWs || !this.frontendWs.connected) {
52293
52371
  this.frontendWs = new FrontendWebSocket(this.api.apiUrl, this.api.apiKey);
@@ -52355,7 +52433,7 @@ function killExistingEdge(lp2) {
52355
52433
  } catch {
52356
52434
  break;
52357
52435
  }
52358
- Bun.sleepSync(100);
52436
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
52359
52437
  }
52360
52438
  try {
52361
52439
  process.kill(pid, "SIGKILL");
@@ -52398,13 +52476,24 @@ async function main() {
52398
52476
  console.error("\x1B[31mAnother edge is already running for this user+server. Use --kill to replace it.\x1B[0m");
52399
52477
  process.exit(1);
52400
52478
  }
52479
+ let cleaned = false;
52401
52480
  const cleanup = () => {
52481
+ if (cleaned)
52482
+ return;
52483
+ cleaned = true;
52484
+ edge.stop();
52402
52485
  unmountAllRclone();
52403
52486
  releaseLock(lp2);
52404
52487
  };
52405
52488
  process.on("exit", cleanup);
52406
- process.on("SIGINT", () => process.exit(0));
52407
- process.on("SIGTERM", () => process.exit(0));
52489
+ process.on("SIGINT", () => {
52490
+ cleanup();
52491
+ process.exit(0);
52492
+ });
52493
+ process.on("SIGTERM", () => {
52494
+ cleanup();
52495
+ process.exit(0);
52496
+ });
52408
52497
  await edge.start();
52409
52498
  }
52410
52499
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.12.13",
3
+ "version": "0.12.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"