@todoforai/edge 0.13.14 → 0.13.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 +123 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -48790,7 +48790,6 @@ var tool_catalog_default = {
48790
48790
  pkg: "tiktok-uploader",
48791
48791
  installer: "pip",
48792
48792
  label: "TikTok",
48793
- statusCmd: "python3 -c 'import tiktok_uploader' 2>&1",
48794
48793
  loginCmd: "npx @todoforai/tiktok-cookie-helper",
48795
48794
  credentialPaths: [
48796
48795
  "~/.tiktok/cookies.txt"
@@ -48806,7 +48805,6 @@ var tool_catalog_default = {
48806
48805
  label: "Instagram",
48807
48806
  capabilities: "Upload photos & reels, post stories, send DMs, like & comment, manage followers",
48808
48807
  description: 'Use for Instagram automation: upload photos/reels, post stories, DMs, like/comment, follower management. Python library; call via `python3 -c "from instagrapi import Client; ..."`.',
48809
- statusCmd: 'python3 -c "import instagrapi" 2>/dev/null',
48810
48808
  versionCmd: "pip show instagrapi 2>/dev/null | grep -oP 'Version: \\K.*'"
48811
48809
  },
48812
48810
  mudslide: {
@@ -48828,7 +48826,6 @@ var tool_catalog_default = {
48828
48826
  pkg: "telegram-send",
48829
48827
  installer: "pip",
48830
48828
  label: "Telegram",
48831
- statusCmd: "test -f ~/.config/telegram-send/telegram-send.conf && echo 'configured'",
48832
48829
  loginCmd: "telegram-send --configure",
48833
48830
  credentialPaths: [
48834
48831
  "~/.config/telegram-send/telegram-send.conf"
@@ -49052,7 +49049,6 @@ var tool_catalog_default = {
49052
49049
  pkg: "@shopify/cli",
49053
49050
  installer: "npm",
49054
49051
  label: "Shopify",
49055
- statusCmd: "test -f ~/.config/shopify/config.json && echo 'authenticated'",
49056
49052
  loginCmd: "shopify auth login",
49057
49053
  credentialPaths: [
49058
49054
  "~/.config/shopify/config.json"
@@ -49152,7 +49148,7 @@ var tool_catalog_default = {
49152
49148
  "~/.config/rclone/rclone.conf"
49153
49149
  ],
49154
49150
  capabilities: "Access Google Drive, OneDrive, Dropbox, S3 and 40+ cloud providers. List, copy, sync, move, mount files as virtual filesystem (FUSE). Use 'rclone config create <name> <provider>' to connect (opens browser for OAuth).",
49155
- description: "Use to access cloud storage (Drive/OneDrive/Dropbox/S3/40+). Connect: `rclone config create <name> <provider>` (OAuth via browser). Core ops: `rclone listremotes`, `rclone ls <remote>:<path>`, `rclone copy|sync <src> <dst>`. Mount as FUSE (lazy fetch): `rclone mount <remote>: ~/.todoforai/mnt/<remote> --vfs-cache-mode full --daemon`; unmount with `fusermount -u <path>`. See `subProviders` for per-provider connect commands.",
49151
+ description: "Use to access cloud storage (Drive/OneDrive/Dropbox/S3/40+). Connect: `rclone config create <name> <provider>` (OAuth via browser). Core ops: `rclone listremotes`, `rclone lsf <remote>:<path>`, `rclone cat <remote>:<file>`, `rclone copy|sync <src> <dst>`. Mount as FUSE (lazy fetch): `rclone mount <remote>: ~/.todoforai/mnt/<remote> --vfs-cache-mode full --daemon`; unmount with `fusermount -u <path>`. Note: FUSE `mount` needs /dev/fuse + CAP_SYS_ADMIN — works on VM sandboxes and bridge devices, but NOT in lite sandboxes; there use `lsf`/`cat`/`copy`/`sync` (HTTPS-only) instead. See `subProviders` for per-provider connect commands.",
49156
49152
  subProviders: {
49157
49153
  gdrive: {
49158
49154
  label: "Google Drive",
@@ -49198,7 +49194,7 @@ var tool_catalog_default = {
49198
49194
  installer: "pip",
49199
49195
  label: "PDF",
49200
49196
  capabilities: "PDF text editing, extraction, merge, split",
49201
- description: "Use to programmatically read/edit/merge/split PDFs (text + positions). Call from `python3 -c`. Extract text: `pymupdf.open(p)[i].get_text()`; with bbox/font: `.get_text('dict')`. Replace text in place: `page.add_redact_annot(page.search_for('old')[0], text='new'); page.apply_redactions(); doc.save(out)`. For PDF → markdown prefer `firecrawl parse`.",
49197
+ description: "Use to programmatically read/edit/merge/split PDFs (text + positions). Call from `python3 -c`. Extract text: `pymupdf.open(p)[i].get_text()`; with bbox/font: `.get_text('dict')`. Replace text in place: `page.add_redact_annot(page.search_for('old')[0], text='new'); page.apply_redactions(); doc.save(out)`.",
49202
49198
  versionCmd: "python3 -c 'import pymupdf; print(pymupdf.__version__)' 2>/dev/null",
49203
49199
  preinstallCloud: true
49204
49200
  },
@@ -49209,23 +49205,9 @@ var tool_catalog_default = {
49209
49205
  preinstallCloud: true,
49210
49206
  label: "Browser",
49211
49207
  capabilities: "Headless browser automation, web scraping, accessibility tree snapshots, screenshots, PDF generation",
49212
- description: "Headless browser for JS-rendered pages, automated flows, screenshots, PDF export. **Default browser choice** — use this unless the user's own session/cookies are required (then use `todoforai-browser`). Prefer `curl` for plain HTTP, `firecrawl` for bulk scrape/crawl. Commands: open <url>, click/type/fill <selector> <text>, snapshot (accessibility tree with @refs — use these for clicking), screenshot [path], pdf <path>, eval <js>, wait <selector|ms>.",
49208
+ description: "Headless browser for JS-rendered pages, automated flows, screenshots, PDF export. **Default browser choice** — use this unless the user's own session/cookies are required (then use `todoforai-browser`). Prefer `curl` for plain HTTP. Commands: open <url>, click/type/fill <selector> <text>, snapshot (accessibility tree with @refs — use these for clicking), screenshot [path], pdf <path>, eval <js>, wait <selector|ms>.",
49213
49209
  versionCmd: "agent-browser --version 2>/dev/null | head -1"
49214
49210
  },
49215
- firecrawl: {
49216
- category: "development",
49217
- pkg: "firecrawl-cli",
49218
- installer: "npm",
49219
- label: "Firecrawl",
49220
- statusCmd: "firecrawl view-config 2>&1 | grep -q 'Authenticated' && echo authenticated",
49221
- loginCmd: "firecrawl login",
49222
- credentialPaths: [
49223
- "~/.config/firecrawl-cli"
49224
- ],
49225
- capabilities: "Web scraping, crawling, search, map URLs, parse local HTML/PDF/DOCX to markdown, AI agent extraction",
49226
- description: "Use for web → markdown at scale: single pages (`firecrawl scrape <url>`), whole sites (`firecrawl crawl <url>`), URL discovery (`firecrawl map <url>`), web search (`firecrawl search <query>`), local doc conversion (`firecrawl parse <file.pdf|docx|html|xlsx>`), AI-guided extraction (`firecrawl agent <prompt>`). Prefer over `curl` when you want markdown not HTML. Auth: `firecrawl login` or FIRECRAWL_API_KEY.",
49227
- versionCmd: "firecrawl --version 2>/dev/null | head -1"
49228
- },
49229
49211
  "todoforai-browser": {
49230
49212
  category: "development",
49231
49213
  pkg: "@todoforai/browser",
@@ -49266,7 +49248,6 @@ var tool_catalog_default = {
49266
49248
  capabilities: "Explore a codebase as a real TODO: read-only agent maps structure, surfaces relevant files, streams findings to terminal",
49267
49249
  description: 'Codebase exploration as a real TODO with patched permissions (read/grep/bash only) and live streaming. Usage: `tfa-explore [--repo <path>] "<question>"`. Creates a TODO visible in the UI and streams block events to stdout until DONE.',
49268
49250
  versionCmd: "tfa-explore --version 2>/dev/null | head -1",
49269
- statusCmd: "tfa-explore whoami",
49270
49251
  installCmd: "bun add -g @todoforai/tfa-explore",
49271
49252
  preinstall: true,
49272
49253
  preinstallCloud: true,
@@ -49280,7 +49261,6 @@ var tool_catalog_default = {
49280
49261
  capabilities: "Review a git diff as a real TODO: read-only agent assesses goal, finds issues, suggests simpler approaches",
49281
49262
  description: 'Capture `git diff` and ask a read-only sub-agent to review it as a real TODO. Usage: `tfa-review [--repo <path>] [--against <ref>] "<goal>"`. Default diffs uncommitted changes vs HEAD. Streams block events to stdout until DONE.',
49282
49263
  versionCmd: "tfa-review --version 2>/dev/null | head -1",
49283
- statusCmd: "tfa-review whoami",
49284
49264
  installCmd: "bun add -g @todoforai/tfa-review",
49285
49265
  preinstall: true,
49286
49266
  preinstallCloud: true,
@@ -49294,7 +49274,6 @@ var tool_catalog_default = {
49294
49274
  capabilities: "Summarize files or piped input as a real TODO with no-tools sub-agent",
49295
49275
  description: 'Summarize file contents or stdin as a real TODO. Usage: `tfa-summary [-f <file>]... [<files...>] [<focus>]` or `cat x | tfa-summary "focus"`. No tools (content is inlined); streams block events to stdout until DONE.',
49296
49276
  versionCmd: "tfa-summary --version 2>/dev/null | head -1",
49297
- statusCmd: "tfa-summary whoami",
49298
49277
  installCmd: "bun add -g @todoforai/tfa-summary",
49299
49278
  internal: true
49300
49279
  },
@@ -51389,12 +51368,7 @@ import { spawn as nodeSpawn } from "child_process";
51389
51368
  // src/shell-pause-detector.ts
51390
51369
  import os5 from "os";
51391
51370
  import { readFile, readlink } from "fs/promises";
51392
-
51393
- class NullDetector {
51394
- watch() {
51395
- return { cancel: () => {}, reset: () => {} };
51396
- }
51397
- }
51371
+ var ECHO = 8;
51398
51372
  var READ_SYSCALL_NR = {
51399
51373
  x64: 0,
51400
51374
  arm64: 63,
@@ -51404,23 +51378,29 @@ var READ_SYSCALL_NR = {
51404
51378
  var READ_NR = READ_SYSCALL_NR[process.arch] ?? -1;
51405
51379
  var POLL_MS = 250;
51406
51380
  var GRACE_TICKS = 2;
51381
+ var IS_LINUX2 = os5.platform() === "linux" && READ_NR >= 0;
51407
51382
 
51408
- class LinuxSyscallDetector {
51409
- watch(pid, onPaused) {
51383
+ class PtyPauseDetector {
51384
+ watch(pid, onPaused, terminal) {
51410
51385
  let pausedTicks = 0;
51411
51386
  let signalled = false;
51412
51387
  let cancelled = false;
51413
51388
  let rootStdin = null;
51414
- readFdTarget(pid, 0).then((t) => {
51415
- rootStdin = t;
51416
- });
51389
+ if (IS_LINUX2)
51390
+ readFdTarget(pid, 0).then((t) => {
51391
+ rootStdin = t;
51392
+ });
51417
51393
  const tick = async () => {
51418
51394
  if (cancelled)
51419
51395
  return;
51420
- const fgPid = await getForegroundPid(pid);
51421
- const sc2 = fgPid != null ? await readSyscall(fgPid) : null;
51422
- const leafTarget = sc2 && sc2.nr === READ_NR && sc2.fd >= 0 && fgPid != null ? await readFdTarget(fgPid, sc2.fd) : null;
51423
- if (isTerminalReadPause(sc2, leafTarget, rootStdin)) {
51396
+ let blocked = terminal != null && !(terminal.localFlags & ECHO);
51397
+ if (!blocked && IS_LINUX2) {
51398
+ const fgPid = await getForegroundPid(pid);
51399
+ const sc2 = fgPid != null ? await readSyscall(fgPid) : null;
51400
+ const leafTarget = sc2 && sc2.nr === READ_NR && sc2.fd >= 0 && fgPid != null ? await readFdTarget(fgPid, sc2.fd) : null;
51401
+ blocked = isTerminalReadPause(sc2, leafTarget, rootStdin);
51402
+ }
51403
+ if (blocked) {
51424
51404
  if (!signalled && ++pausedTicks >= GRACE_TICKS) {
51425
51405
  signalled = true;
51426
51406
  onPaused();
@@ -51491,7 +51471,7 @@ async function readFdTarget(pid, fd3) {
51491
51471
  return null;
51492
51472
  }
51493
51473
  }
51494
- var pauseDetector = os5.platform() === "linux" && READ_NR >= 0 ? new LinuxSyscallDetector : new NullDetector;
51474
+ var pauseDetector = new PtyPauseDetector;
51495
51475
 
51496
51476
  // src/shell.ts
51497
51477
  var IS_WIN = os6.platform() === "win32";
@@ -51651,13 +51631,13 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51651
51631
  }
51652
51632
  }, timeout * 1000);
51653
51633
  let cancelPauseWatch = null;
51654
- const startPauseWatch = (pid) => {
51634
+ const startPauseWatch = (pid, terminal) => {
51655
51635
  if (!keepAliveOnTimeout)
51656
51636
  return;
51657
51637
  const watcher = pauseDetector.watch(pid, () => {
51658
51638
  if (processes.has(blockId))
51659
51639
  resolveAlive();
51660
- });
51640
+ }, terminal);
51661
51641
  cancelPauseWatch = watcher.cancel;
51662
51642
  const h = processes.get(blockId);
51663
51643
  if (h)
@@ -51707,7 +51687,7 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51707
51687
  const handle = { terminal, proc, pid: proc.pid };
51708
51688
  processes.set(blockId, handle);
51709
51689
  const timer = startTimeout();
51710
- startPauseWatch(proc.pid);
51690
+ startPauseWatch(proc.pid, terminal);
51711
51691
  proc.exited.then((code) => {
51712
51692
  terminal.close();
51713
51693
  onExit(code ?? -1, timer);
@@ -52465,6 +52445,106 @@ register("read_file_base64", async (args) => {
52465
52445
  const data = fs11.readFileSync(fullPath);
52466
52446
  return { path: fullPath, base64: data.toString("base64"), bytes: data.length };
52467
52447
  });
52448
+ register("search_files", async (args) => {
52449
+ const { pattern, path: p10 = ".", cwd = args.root_path ?? "", head = 100, max_count = 5, glob: globPattern = "", ignore_case = true } = args;
52450
+ const { execSync: execWhich } = await import("child_process");
52451
+ const whichCmd = process.platform === "win32" ? "where" : "which";
52452
+ const which = (bin) => {
52453
+ try {
52454
+ return execWhich(`${whichCmd} ${bin}`, { encoding: "utf-8" }).trim().split(`
52455
+ `)[0].trim();
52456
+ } catch {
52457
+ return null;
52458
+ }
52459
+ };
52460
+ let rgPath = which("rg");
52461
+ if (!rgPath) {
52462
+ await ensureTool("rg");
52463
+ rgPath = which("rg");
52464
+ }
52465
+ let searchPath = p10.replace(/^~/, process.env.HOME || "~");
52466
+ if (!path8.isAbsolute(searchPath) && cwd)
52467
+ searchPath = path8.join(cwd, searchPath);
52468
+ searchPath = path8.resolve(searchPath);
52469
+ if (!fs11.existsSync(searchPath))
52470
+ throw new Error(`Search path does not exist: ${searchPath}`);
52471
+ let cmd;
52472
+ if (rgPath) {
52473
+ cmd = [rgPath, "--no-heading", "--line-number", "--color=never"];
52474
+ if (ignore_case)
52475
+ cmd.push("--ignore-case");
52476
+ if (max_count > 0)
52477
+ cmd.push(`--max-count=${max_count}`);
52478
+ if (globPattern)
52479
+ cmd.push("--glob", globPattern);
52480
+ cmd.push(pattern, searchPath);
52481
+ } else {
52482
+ console.warn("[search_files] ripgrep (rg) not found, falling back to grep");
52483
+ const grepPath = which("grep") || "grep";
52484
+ cmd = [grepPath, "-rn", "--color=never"];
52485
+ if (ignore_case)
52486
+ cmd.push("-i");
52487
+ if (max_count > 0)
52488
+ cmd.push(`--max-count=${max_count}`);
52489
+ if (globPattern) {
52490
+ cmd.push(`--include=${globPattern}`);
52491
+ }
52492
+ cmd.push(pattern, searchPath);
52493
+ }
52494
+ const { spawn: spawnChild } = await import("child_process");
52495
+ const { stdout, stderr, code } = await new Promise((resolve) => {
52496
+ const child = spawnChild(cmd[0], cmd.slice(1));
52497
+ let out = "", err2 = "";
52498
+ child.stdout?.on("data", (d) => {
52499
+ out += d.toString();
52500
+ });
52501
+ child.stderr?.on("data", (d) => {
52502
+ err2 += d.toString();
52503
+ });
52504
+ child.on("close", (exitCode) => resolve({ stdout: out, stderr: err2, code: exitCode ?? 1 }));
52505
+ });
52506
+ if (code === 0) {
52507
+ let output = stdout;
52508
+ const lines = output.split(`
52509
+ `).filter((l) => l.trim());
52510
+ if (lines.length > head) {
52511
+ output = lines.slice(0, head).join(`
52512
+ `) + `
52513
+ ... (${lines.length - head} more matches truncated)`;
52514
+ }
52515
+ if ((cwd || searchPath) && output) {
52516
+ const searchBase = searchPath && fs11.existsSync(searchPath) && fs11.statSync(searchPath).isDirectory() ? searchPath : path8.dirname(searchPath);
52517
+ const bases = Array.from(new Set([cwd, searchBase].filter(Boolean)));
52518
+ const lines2 = output.split(`
52519
+ `).map((line) => {
52520
+ if (line.includes(":")) {
52521
+ const colonIdx = line.indexOf(":");
52522
+ let filePart = line.slice(0, colonIdx);
52523
+ const rest = line.slice(colonIdx);
52524
+ try {
52525
+ const candidates = [filePart, ...bases.map((b) => path8.relative(b, filePart))].filter((p11) => (p11.match(/\.\.\//g) || []).length <= 2);
52526
+ filePart = candidates.reduce((a, b) => a.length <= b.length ? a : b, filePart);
52527
+ } catch {}
52528
+ let fullLine = filePart + rest;
52529
+ if (fullLine.length > 300) {
52530
+ fullLine = fullLine.slice(0, 300) + "...";
52531
+ }
52532
+ return fullLine;
52533
+ }
52534
+ return line;
52535
+ });
52536
+ output = lines2.join(`
52537
+ `);
52538
+ }
52539
+ if (output.length > 1e5)
52540
+ output = output.slice(0, 1e5) + `
52541
+ ... (output truncated)`;
52542
+ return { result: output };
52543
+ }
52544
+ if (code === 1)
52545
+ return { result: "No matches found." };
52546
+ throw new Error(`search error (exit ${code}): ${stderr}`);
52547
+ });
52468
52548
  register("download_attachment", async (args, client) => {
52469
52549
  if (!client)
52470
52550
  throw new Error("Client instance required");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.13.14",
3
+ "version": "0.13.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"