@todoforai/edge 0.13.10 → 0.13.12

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 +117 -108
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -47927,7 +47927,6 @@ function credentialsPath() {
47927
47927
  return path.join(xdg, "todoforai", "credentials.json");
47928
47928
  }
47929
47929
  var CREDENTIALS_PATH = credentialsPath();
47930
- var LEGACY_CREDENTIALS_PATH = path.join(os.homedir(), ".todoforai", "credentials.json");
47931
47930
  function readFileMap(p) {
47932
47931
  try {
47933
47932
  return JSON.parse(fs.readFileSync(p, "utf-8"));
@@ -47935,9 +47934,6 @@ function readFileMap(p) {
47935
47934
  return {};
47936
47935
  }
47937
47936
  }
47938
- function readCredentials() {
47939
- return { ...readFileMap(LEGACY_CREDENTIALS_PATH), ...readFileMap(CREDENTIALS_PATH) };
47940
- }
47941
47937
  function writeNewFile(creds) {
47942
47938
  const dir = path.dirname(CREDENTIALS_PATH);
47943
47939
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
@@ -47947,7 +47943,7 @@ function writeNewFile(creds) {
47947
47943
  } catch {}
47948
47944
  }
47949
47945
  function loadSavedApiKey(apiUrl) {
47950
- return readCredentials()[apiUrl] || null;
47946
+ return readFileMap(CREDENTIALS_PATH)[apiUrl] || null;
47951
47947
  }
47952
47948
  function saveApiKey(apiUrl, apiKey) {
47953
47949
  const creds = readFileMap(CREDENTIALS_PATH);
@@ -47960,13 +47956,6 @@ function clearApiKey(apiUrl) {
47960
47956
  delete creds[apiUrl];
47961
47957
  writeNewFile(creds);
47962
47958
  }
47963
- const legacy = readFileMap(LEGACY_CREDENTIALS_PATH);
47964
- if (apiUrl in legacy) {
47965
- delete legacy[apiUrl];
47966
- try {
47967
- fs.writeFileSync(LEGACY_CREDENTIALS_PATH, JSON.stringify(legacy, null, 2), { mode: 384 });
47968
- } catch {}
47969
- }
47970
47959
  }
47971
47960
 
47972
47961
  // node_modules/ws/wrapper.mjs
@@ -48940,7 +48929,7 @@ var tool_catalog_default = {
48940
48929
  pkg: "netlify-cli",
48941
48930
  installer: "npm",
48942
48931
  label: "Netlify",
48943
- statusCmd: `test -f ~/.netlify/config.json && netlify api getCurrentUser 2>&1 | grep -oP '"email":\\s*"\\K[^"]+' | head -1`,
48932
+ statusCmd: `netlify api getCurrentUser 2>&1 | grep -oP '"email":\\s*"\\K[^"]+' | head -1`,
48944
48933
  loginCmd: "netlify login",
48945
48934
  credentialPaths: [
48946
48935
  "~/.netlify/config.json"
@@ -48954,7 +48943,7 @@ var tool_catalog_default = {
48954
48943
  pkg: "firebase-tools",
48955
48944
  installer: "npm",
48956
48945
  label: "Firebase",
48957
- statusCmd: "test -f ~/.config/configstore/firebase-tools.json && firebase login:list 2>&1 | grep '@'",
48946
+ statusCmd: "firebase login:list 2>&1 | grep '@'",
48958
48947
  loginCmd: "firebase login",
48959
48948
  credentialPaths: [
48960
48949
  "~/.config/configstore/firebase-tools.json"
@@ -48968,7 +48957,7 @@ var tool_catalog_default = {
48968
48957
  pkg: "wrangler",
48969
48958
  installer: "npm",
48970
48959
  label: "Cloudflare",
48971
- statusCmd: "test -f ~/.config/.wrangler/config/default.toml && wrangler whoami 2>&1 | grep -v 'Failed' | grep '@'",
48960
+ statusCmd: "wrangler whoami 2>&1 | grep -v 'Failed' | grep '@'",
48972
48961
  loginCmd: "wrangler login",
48973
48962
  credentialPaths: [
48974
48963
  "~/.config/.wrangler/config/default.toml"
@@ -49010,7 +48999,7 @@ var tool_catalog_default = {
49010
48999
  pkg: "supabase",
49011
49000
  installer: "binary",
49012
49001
  label: "Supabase",
49013
- statusCmd: "test -f ~/.config/supabase/access-token && echo 'authenticated'",
49002
+ statusCmd: "supabase projects list >/dev/null 2>&1 && echo authenticated",
49014
49003
  loginCmd: "supabase login",
49015
49004
  credentialPaths: [
49016
49005
  "~/.config/supabase/access-token"
@@ -49024,7 +49013,7 @@ var tool_catalog_default = {
49024
49013
  pkg: "@railway/cli",
49025
49014
  installer: "npm",
49026
49015
  label: "Railway",
49027
- statusCmd: "test -f ~/.railway/config.json && railway whoami 2>&1 | grep -v 'Unauthorized'",
49016
+ statusCmd: "railway whoami 2>&1 | grep -v 'Unauthorized'",
49028
49017
  loginCmd: "railway login",
49029
49018
  credentialPaths: [
49030
49019
  "~/.railway/config.json"
@@ -49061,7 +49050,7 @@ var tool_catalog_default = {
49061
49050
  pkg: "@sentry/cli",
49062
49051
  installer: "npm",
49063
49052
  label: "Sentry",
49064
- statusCmd: "test -f ~/.sentryclirc && echo 'authenticated'",
49053
+ statusCmd: "sentry-cli info 2>&1 | grep -q 'Authentication Info' && echo authenticated",
49065
49054
  loginCmd: "sentry-cli login",
49066
49055
  credentialPaths: [
49067
49056
  "~/.sentryclirc"
@@ -49077,7 +49066,7 @@ var tool_catalog_default = {
49077
49066
  label: "TODOforAI",
49078
49067
  capabilities: "Create & manage TODOs, run workflows, API access",
49079
49068
  description: "Use to programmatically create/list/update TODOs in TODOforAI, kick off workflows, call the platform API.",
49080
- statusCmd: "todoai --help >/dev/null 2>&1",
49069
+ statusCmd: "todoai whoami >/dev/null 2>&1",
49081
49070
  installCmd: "bun add -g @todoforai/cli",
49082
49071
  versionCmd: "todoai --version 2>/dev/null | head -1",
49083
49072
  internal: true
@@ -49087,7 +49076,6 @@ var tool_catalog_default = {
49087
49076
  pkg: "newman",
49088
49077
  installer: "npm",
49089
49078
  label: "Postman",
49090
- statusCmd: "newman --version",
49091
49079
  capabilities: "Run API test collections, CI/CD integration, HTML reports",
49092
49080
  description: "Use to run Postman collections from CLI (API smoke tests, CI checks). `newman run <collection.json> -e <env.json>`.",
49093
49081
  versionCmd: "newman --version 2>/dev/null | head -1"
@@ -49098,7 +49086,6 @@ var tool_catalog_default = {
49098
49086
  installer: "system",
49099
49087
  label: "Web Request",
49100
49088
  binName: "curl",
49101
- statusCmd: "curl --version >/dev/null 2>&1",
49102
49089
  capabilities: "HTTP/HTTPS/FTP client: GET/POST/PUT/DELETE, headers, auth, file upload/download, follow redirects, cookies, TLS.",
49103
49090
  description: "Default tool for any HTTP request: hitting REST APIs, downloading files, testing endpoints. Common flags: `-s` silent, `-L` follow redirects, `-H 'Header: ...'`, `-X POST -d '...'`, `-o file`, `-f` fail on HTTP errors. Pipe into `jq` for JSON.",
49104
49091
  versionCmd: "curl --version 2>/dev/null | head -1",
@@ -49110,7 +49097,6 @@ var tool_catalog_default = {
49110
49097
  pkg: "cloudflared",
49111
49098
  installer: "binary",
49112
49099
  label: "Tunnel",
49113
- statusCmd: "cloudflared --version",
49114
49100
  capabilities: "Tunnel to localhost, expose local services, zero-trust access",
49115
49101
  description: "Use to expose a local port to the public internet via a Cloudflare tunnel. Quick: `cloudflared tunnel --url http://localhost:<port>` prints a public https URL.",
49116
49102
  versionCmd: "cloudflared --version 2>/dev/null | head -1"
@@ -49121,13 +49107,13 @@ var tool_catalog_default = {
49121
49107
  installer: "binary",
49122
49108
  preinstallCloud: true,
49123
49109
  label: "HashiCorp Vault",
49124
- statusCmd: "test -f ~/.vault-token && echo 'authenticated'",
49110
+ statusCmd: "vault token lookup >/dev/null 2>&1 && echo authenticated",
49125
49111
  loginCmd: "vault login",
49126
49112
  credentialPaths: [
49127
49113
  "~/.vault-token"
49128
49114
  ],
49129
49115
  capabilities: "Secrets management, dynamic credentials",
49130
- description: "HashiCorp Vault CLI for Terraform/vals/ecosystem integrations. For day-to-day secret read/write in TODOforAI, prefer `tfa-vault` (zero-config). Needs VAULT_ADDR + VAULT_TOKEN.",
49116
+ description: "HashiCorp Vault CLI — only for Terraform/vals/ecosystem integrations that expect it. For all credential read/write tasks use `tfa-vault` instead. Needs VAULT_ADDR + VAULT_TOKEN.",
49131
49117
  versionCmd: "vault --version 2>/dev/null | head -1"
49132
49118
  },
49133
49119
  rclone: {
@@ -49136,16 +49122,17 @@ var tool_catalog_default = {
49136
49122
  installer: "system",
49137
49123
  preinstallCloud: true,
49138
49124
  label: "Cloud Sync",
49139
- statusCmd: "rclone listremotes 2>&1 | grep -v 'NOTICE' | head -5",
49125
+ statusCmd: "rclone listremotes 2>&1 | grep -v 'NOTICE' | grep -q ':' && echo authenticated",
49140
49126
  loginCmd: "rclone config",
49141
49127
  credentialPaths: [
49142
49128
  "~/.config/rclone/rclone.conf"
49143
49129
  ],
49144
49130
  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).",
49145
- description: "Connect providers: rclone config create gdrive drive (Google Drive), rclone config create onedrive onedrive (OneDrive), rclone config create dropbox dropbox (Dropbox), rclone config create s3 s3 (Amazon S3). List remotes: rclone listremotes. List files: rclone ls remote:path. Copy: rclone copy source:path dest:path. Sync: rclone sync source:path dest:path. Mount as virtual filesystem (FUSE, no files downloaded until accessed): mkdir -p ~/.todoforai/mnt/<remote> && rclone mount <remote>: ~/.todoforai/mnt/<remote> --vfs-cache-mode full --vfs-fast-fingerprint --no-modtime --attr-timeout 1h --vfs-cache-max-size 400M --daemon --log-file=/tmp/rclone-<remote>.log --log-level INFO. Unmount: fusermount -u ~/.todoforai/mnt/<remote>. Check config: rclone config show.",
49131
+ 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.",
49146
49132
  subProviders: {
49147
49133
  gdrive: {
49148
49134
  label: "Google Drive",
49135
+ authType: "oauth",
49149
49136
  connectCmd: "rclone config create {{REMOTE}} drive",
49150
49137
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49151
49138
  remoteName: "gdrive",
@@ -49154,6 +49141,7 @@ var tool_catalog_default = {
49154
49141
  },
49155
49142
  onedrive: {
49156
49143
  label: "OneDrive",
49144
+ authType: "oauth",
49157
49145
  connectCmd: "rclone config create {{REMOTE}} onedrive",
49158
49146
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49159
49147
  remoteName: "onedrive",
@@ -49162,6 +49150,7 @@ var tool_catalog_default = {
49162
49150
  },
49163
49151
  dropbox: {
49164
49152
  label: "Dropbox",
49153
+ authType: "oauth",
49165
49154
  connectCmd: "rclone config create {{REMOTE}} dropbox",
49166
49155
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49167
49156
  remoteName: "dropbox",
@@ -49170,6 +49159,7 @@ var tool_catalog_default = {
49170
49159
  },
49171
49160
  s3: {
49172
49161
  label: "Amazon S3",
49162
+ authType: "credentials",
49173
49163
  connectCmd: "rclone config create {{REMOTE}} s3",
49174
49164
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49175
49165
  remoteName: "s3",
@@ -49183,7 +49173,6 @@ var tool_catalog_default = {
49183
49173
  pkg: "pymupdf",
49184
49174
  installer: "pip",
49185
49175
  label: "PDF",
49186
- statusCmd: "python3 -c 'import pymupdf' 2>/dev/null || /usr/bin/python3 -c 'import pymupdf' 2>/dev/null",
49187
49176
  capabilities: "PDF text editing, extraction, merge, split",
49188
49177
  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`.",
49189
49178
  versionCmd: "python3 -c 'import pymupdf; print(pymupdf.__version__)' 2>/dev/null",
@@ -49195,9 +49184,8 @@ var tool_catalog_default = {
49195
49184
  installer: "npm",
49196
49185
  preinstallCloud: true,
49197
49186
  label: "Browser",
49198
- statusCmd: "agent-browser --version",
49199
49187
  capabilities: "Headless browser automation, web scraping, accessibility tree snapshots, screenshots, PDF generation",
49200
- description: "Use when a task needs a real browser: JS-rendered pages, logged-in flows, clicking/filling forms, screenshots, PDF export. Prefer `curl` for plain HTTP and `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>.",
49188
+ 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>.",
49201
49189
  versionCmd: "agent-browser --version 2>/dev/null | head -1"
49202
49190
  },
49203
49191
  firecrawl: {
@@ -49220,7 +49208,7 @@ var tool_catalog_default = {
49220
49208
  installer: "npm",
49221
49209
  label: "Browser (Extension)",
49222
49210
  capabilities: "Browser automation via extension (non-headless), web scraping, accessibility tree snapshots, screenshots, PDF generation",
49223
- description: "Browser automation via the TODO for AI browser extension (non-headless, uses your real browser). Commands: open <url>, click/type/fill <selector> <text>, snapshot (accessibility tree with refs), screenshot [path], pdf <path>, eval <js>, wait <selector|ms>. Use snapshot to get semantic page structure with @refs for clicking. Supports drag/drop, file uploads, form interactions.",
49211
+ description: "Drives the user's real browser via the TODOforAI extension (non-headless). Use when the task needs the user's actual session — logged-in accounts, saved cookies, extensions, MFA. For anything else prefer headless `agent-browser`. Commands: open <url>, click/type/fill <selector> <text>, snapshot (accessibility tree with @refs for clicking), screenshot [path], pdf <path>, eval <js>, wait <selector|ms>. Supports drag/drop, file uploads, form interactions.",
49224
49212
  versionCmd: `node -p "require(require('child_process').execSync('which todoforai-browser',{encoding:'utf8'}).trim().replace(/\\/dist\\/index\\.js$/, '/package.json')).version" 2>/dev/null`,
49225
49213
  internal: true
49226
49214
  },
@@ -49254,6 +49242,7 @@ var tool_catalog_default = {
49254
49242
  capabilities: "Explore a codebase as a real TODO: read-only agent maps structure, surfaces relevant files, streams findings to terminal",
49255
49243
  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.',
49256
49244
  versionCmd: "tfa-explore --version 2>/dev/null | head -1",
49245
+ statusCmd: "tfa-explore whoami",
49257
49246
  installCmd: "bun add -g @todoforai/tfa-explore",
49258
49247
  preinstall: true,
49259
49248
  preinstallCloud: true,
@@ -49267,6 +49256,7 @@ var tool_catalog_default = {
49267
49256
  capabilities: "Review a git diff as a real TODO: read-only agent assesses goal, finds issues, suggests simpler approaches",
49268
49257
  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.',
49269
49258
  versionCmd: "tfa-review --version 2>/dev/null | head -1",
49259
+ statusCmd: "tfa-review whoami",
49270
49260
  installCmd: "bun add -g @todoforai/tfa-review",
49271
49261
  preinstall: true,
49272
49262
  preinstallCloud: true,
@@ -49280,6 +49270,7 @@ var tool_catalog_default = {
49280
49270
  capabilities: "Summarize files or piped input as a real TODO with no-tools sub-agent",
49281
49271
  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.',
49282
49272
  versionCmd: "tfa-summary --version 2>/dev/null | head -1",
49273
+ statusCmd: "tfa-summary whoami",
49283
49274
  installCmd: "bun add -g @todoforai/tfa-summary",
49284
49275
  internal: true
49285
49276
  },
@@ -49289,13 +49280,12 @@ var tool_catalog_default = {
49289
49280
  installer: "npm",
49290
49281
  label: "Vault",
49291
49282
  capabilities: "Zero-config TODOforAI vault CLI: put/get/patch/list/rm secrets in the user's KV v2 vault. Reuses bridge credentials (TODOFORAI_API_KEY / ~/.config/todoforai/credentials.json); derives vault URL from backend URL.",
49292
- description: 'Read & write secrets in the user\'s TODOforAI vault no VAULT_ADDR, no VAULT_TOKEN, no `vault login` needed. Usage: `tfa-vault put <path> k=v [k=v ...]`, `tfa-vault get [-f <field>] <path>`, `tfa-vault patch <path> k=v ...`, `tfa-vault list [<prefix>]`, `tfa-vault rm <path>`. Values: inline (`k=v`), file (`k=@/path`), stdin (`k=-`). `get -f <field>` writes raw value with no trailing newline — safe in `$(...)`. Exit 2 means "not found" (distinct from network errors). Prefer this over the HashiCorp `vault` CLI for daily use; `vault` remains supported for Terraform/vals/ecosystem tools.',
49283
+ description: 'Native TODOforAI secret store free, zero-config (reuses bridge credentials; no VAULT_ADDR/VAULT_TOKEN/login). **Default credential source: for any task touching API keys, tokens, service accounts, or third-party auth, check `tfa-vault` before searching repo files, .env, or env vars — unless the user provides another source.** Discover-then-consume pattern: `tfa-vault list accounts/` to find keys, then `KEY=$(tfa-vault get -f api_key accounts/<service>)` in shell. Common paths: `accounts/<service>`, `api/<service>`, `secrets/<service>`. Usage: `tfa-vault put <path> k=v [k=v ...]`, `tfa-vault get [-f <field>] <path>`, `tfa-vault patch <path> k=v ...`, `tfa-vault list [<prefix>]`, `tfa-vault rm <path>`. Values: inline (`k=v`), file (`k=@/path`), stdin (`k=-`). `get -f <field>` writes raw value with no trailing newline — safe in `$(...)`. Exit 2 means "not found" (distinct from network errors).',
49293
49284
  versionCmd: "tfa-vault --version 2>/dev/null | head -1",
49294
49285
  statusCmd: "tfa-vault whoami",
49295
49286
  installCmd: "bun add -g @todoforai/vault",
49296
49287
  preinstall: true,
49297
- preinstallCloud: true,
49298
- internal: true
49288
+ preinstallCloud: true
49299
49289
  },
49300
49290
  ripgrep: {
49301
49291
  category: "development",
@@ -49531,17 +49521,6 @@ function isToolInstalled(name) {
49531
49521
  }
49532
49522
  return whichWithTools(name) !== null;
49533
49523
  }
49534
- function findReferencedTools(content) {
49535
- const stripped = content.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
49536
- return Object.keys(TOOL_CATALOG).filter((name) => {
49537
- const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49538
- const re = new RegExp(String.raw`(?:^|[|;&\n]|&&|\|\||` + String.raw`\$\(|` + "`" + String.raw`|xargs\s+|sudo\s+|env\s+)\s*` + esc + String.raw`\b(?!-)`, "m");
49539
- return re.test(stripped);
49540
- });
49541
- }
49542
- function findMissingTools(content) {
49543
- return findReferencedTools(content).filter((name) => !isToolInstalled(name));
49544
- }
49545
49524
  async function installBinary(name) {
49546
49525
  const urlFunc = BINARY_URL_FUNCS[name];
49547
49526
  if (!urlFunc) {
@@ -51592,7 +51571,6 @@ ${this.lastPart}`;
51592
51571
  var processes = new Map;
51593
51572
  var outputBuffers = new Map;
51594
51573
  var completionResolvers = new Map;
51595
- var pendingToolApprovals = new Map;
51596
51574
  var exitedOutputByPid = new Map;
51597
51575
  async function executeBlock(blockId, content, send, todoId, messageId, timeout, cwd, manual = false, runMode, edgeId, agentSettingsId = "", keepAliveOnTimeout = false) {
51598
51576
  if (processes.has(blockId)) {
@@ -51611,38 +51589,6 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51611
51589
  } else {
51612
51590
  cwd = tmpDir;
51613
51591
  }
51614
- const missing = findMissingTools(content);
51615
- if (missing.length && !pendingToolApprovals.has(blockId)) {
51616
- pendingToolApprovals.set(blockId, missing);
51617
- console.log(`[shell] Missing tools ${missing}, requesting approval for block ${blockId}`);
51618
- await send({
51619
- type: "BLOCK_UPDATE",
51620
- payload: {
51621
- todoId,
51622
- blockId,
51623
- messageId,
51624
- updates: {
51625
- status: "AWAITING_APPROVAL",
51626
- approvalContext: { source: "edge", toolInstalls: missing, workspace: cwd, edgeId }
51627
- }
51628
- }
51629
- });
51630
- return;
51631
- }
51632
- if (pendingToolApprovals.has(blockId)) {
51633
- const tools = pendingToolApprovals.get(blockId);
51634
- pendingToolApprovals.delete(blockId);
51635
- const installed = [];
51636
- for (const t of tools) {
51637
- if (await ensureTool(t))
51638
- installed.push(t);
51639
- }
51640
- if (installed.length) {
51641
- const notice = `[installed: ${installed.join(", ")}]
51642
- `;
51643
- await send(msg.shellBlockResult(todoId, blockId, notice, messageId));
51644
- }
51645
- }
51646
51592
  await send({
51647
51593
  type: "BLOCK_UPDATE",
51648
51594
  payload: { todoId, blockId, messageId, updates: { status: "RUNNING" } }
@@ -51931,20 +51877,13 @@ var MAX_DIRS_PER_ROOT = 2000;
51931
51877
  var FRONTMATTER_BYTES = 8 * 1024;
51932
51878
  var MAX_NAME_LEN = 64;
51933
51879
  var MAX_DESC_LEN = 1024;
51934
- function shortestPath(filePath, relTo) {
51935
- const rel = path6.relative(relTo, filePath);
51936
- const home = os7.homedir();
51937
- const homeRel = filePath.startsWith(home + path6.sep) ? "~" + filePath.slice(home.length) : filePath;
51938
- return [rel, homeRel, filePath].reduce((a, b) => b.length < a.length ? b : a);
51939
- }
51940
51880
  async function discoverSkills(rootPaths, opts = {}) {
51941
51881
  const includeUserScope = opts.includeUserScope ?? true;
51942
51882
  const roots = [
51943
- ...rootPaths.map((p10) => ({ path: path6.join(p10, ".agents", "skills"), relTo: p10, scope: "repo" }))
51883
+ ...rootPaths.map((p10) => ({ path: path6.join(p10, ".agents", "skills"), scope: "repo" }))
51944
51884
  ];
51945
51885
  if (includeUserScope) {
51946
- const home = os7.homedir();
51947
- roots.push({ path: path6.join(home, ".agents", "skills"), relTo: home, scope: "user" });
51886
+ roots.push({ path: path6.join(os7.homedir(), ".agents", "skills"), scope: "user" });
51948
51887
  }
51949
51888
  const skills = [];
51950
51889
  const errors = [];
@@ -51962,11 +51901,11 @@ async function discoverSkills(rootPaths, opts = {}) {
51962
51901
  }
51963
51902
  if (!stat.isDirectory())
51964
51903
  continue;
51965
- walkRoot(root.path, root.relTo, root.scope, skills, errors, seenSkillPaths);
51904
+ walkRoot(root.path, root.scope, skills, errors, seenSkillPaths);
51966
51905
  }
51967
51906
  return { skills, errors };
51968
51907
  }
51969
- function walkRoot(root, relTo, scope, skills, errors, seen) {
51908
+ function walkRoot(root, scope, skills, errors, seen) {
51970
51909
  const queue = [{ dir: root, depth: 0 }];
51971
51910
  let dirsVisited = 0;
51972
51911
  for (let i10 = 0;i10 < queue.length; i10++) {
@@ -52002,14 +51941,14 @@ function walkRoot(root, relTo, scope, skills, errors, seen) {
52002
51941
  if (seen.has(full))
52003
51942
  continue;
52004
51943
  seen.add(full);
52005
- const skill = parseSkillFile(full, relTo, scope, errors);
51944
+ const skill = parseSkillFile(full, scope, errors);
52006
51945
  if (skill)
52007
51946
  skills.push(skill);
52008
51947
  }
52009
51948
  }
52010
51949
  }
52011
51950
  }
52012
- function parseSkillFile(filePath, relTo, scope, errors) {
51951
+ function parseSkillFile(filePath, scope, errors) {
52013
51952
  let head;
52014
51953
  try {
52015
51954
  const fd3 = fs9.openSync(filePath, "r");
@@ -52042,7 +51981,7 @@ function parseSkillFile(filePath, relTo, scope, errors) {
52042
51981
  name,
52043
51982
  description,
52044
51983
  shortDescription: shortDescription && shortDescription.length <= MAX_DESC_LEN ? shortDescription : undefined,
52045
- path: shortestPath(filePath, relTo),
51984
+ path: filePath,
52046
51985
  scope
52047
51986
  };
52048
51987
  }
@@ -52372,9 +52311,6 @@ register("execute_shell_command", async (args, client) => {
52372
52311
  try {
52373
52312
  await send(msg.shellBlockStart(todoId, blockId, "execute", messageId));
52374
52313
  await executeBlock(blockId, execCmd, send, todoId, messageId, timeout, cwd, false, "internal", undefined, agentSettingsId, true);
52375
- if (pendingToolApprovals.has(blockId)) {
52376
- return { __awaiting_approval__: true };
52377
- }
52378
52314
  await waitForCompletion(blockId, (timeout + 5) * 1000);
52379
52315
  const rawOutput = getBlockRawOutput(blockId);
52380
52316
  let output = rawOutput ?? getBlockOutput(blockId);
@@ -52686,8 +52622,6 @@ async function handleFunctionCall(payload, send, client) {
52686
52622
  throw new Error(`Unknown function: ${functionName}. Available: ${available.join(", ")}`);
52687
52623
  }
52688
52624
  const result = await fn2(args, client);
52689
- if (result && result.__awaiting_approval__)
52690
- return;
52691
52625
  await send(makeSuccess(result));
52692
52626
  } catch (e) {
52693
52627
  log3("error", `Function call '${functionName}' failed:`, e.message);
@@ -53186,22 +53120,82 @@ class ServerError extends Error {
53186
53120
  }
53187
53121
  }
53188
53122
 
53123
+ // node_modules/@todoforai/update-notifier/src/index.ts
53124
+ import fs12 from "node:fs";
53125
+ import path9 from "node:path";
53126
+ import os9 from "node:os";
53127
+ function isLinkedInstall() {
53128
+ try {
53129
+ return !fs12.realpathSync(process.argv[1] || "").includes(`${path9.sep}node_modules${path9.sep}`);
53130
+ } catch {
53131
+ return false;
53132
+ }
53133
+ }
53134
+ var TTL_MS = 24 * 60 * 60 * 1000;
53135
+ var CACHE_DIR = path9.join(os9.homedir(), ".config", "todoforai");
53136
+ function cmpVer(a, b) {
53137
+ const pa2 = a.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
53138
+ const pb2 = b.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
53139
+ for (let i10 = 0;i10 < Math.max(pa2.length, pb2.length); i10++) {
53140
+ const d = (pa2[i10] ?? 0) - (pb2[i10] ?? 0);
53141
+ if (d)
53142
+ return d;
53143
+ }
53144
+ return 0;
53145
+ }
53146
+ function checkForUpdates(pkg) {
53147
+ if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER || isLinkedInstall())
53148
+ return;
53149
+ const cacheFile = path9.join(CACHE_DIR, `notifier-${encodeURIComponent(pkg.name)}.json`);
53150
+ let cache = {};
53151
+ try {
53152
+ cache = JSON.parse(fs12.readFileSync(cacheFile, "utf8"));
53153
+ } catch {}
53154
+ if (cache.latest && cmpVer(cache.latest, pkg.version) > 0) {
53155
+ process.stderr.write(`
53156
+ \x1B[33m Update available: \x1B[2m${pkg.version}\x1B[22m → \x1B[1m${cache.latest}\x1B[0m
53157
+ ` + `\x1B[33m Run:\x1B[0m npm i -g ${pkg.name}
53158
+
53159
+ `);
53160
+ }
53161
+ if (Date.now() - (cache.ts ?? 0) > TTL_MS) {
53162
+ try {
53163
+ fs12.mkdirSync(CACHE_DIR, { recursive: true });
53164
+ fs12.writeFileSync(cacheFile, JSON.stringify({ ...cache, ts: Date.now() }));
53165
+ } catch {}
53166
+ fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) }).then((r) => r.ok ? r.json() : null).then((j) => {
53167
+ if (!j?.version)
53168
+ return;
53169
+ fs12.writeFileSync(cacheFile, JSON.stringify({ ts: Date.now(), latest: j.version }));
53170
+ }).catch(() => {});
53171
+ }
53172
+ }
53173
+
53189
53174
  // src/index.ts
53190
- import fs12 from "fs";
53191
- import path9 from "path";
53192
- import os9 from "os";
53175
+ import { fileURLToPath as fileURLToPath2 } from "url";
53176
+ import fs13 from "fs";
53177
+ import path10 from "path";
53178
+ import os10 from "os";
53193
53179
  import crypto3 from "crypto";
53180
+ function readOwnPackage() {
53181
+ try {
53182
+ const pkgPath = path10.resolve(fileURLToPath2(import.meta.url), "../../package.json");
53183
+ return JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
53184
+ } catch {
53185
+ return null;
53186
+ }
53187
+ }
53194
53188
  function lockPath(apiUrl, userId) {
53195
- const dir = path9.join(os9.homedir(), ".todoforai");
53196
- fs12.mkdirSync(dir, { recursive: true });
53189
+ const dir = path10.join(os10.homedir(), ".todoforai");
53190
+ fs13.mkdirSync(dir, { recursive: true });
53197
53191
  const hash = crypto3.createHash("sha256").update(`${apiUrl}
53198
53192
  ${userId}`).digest("hex").slice(0, 12);
53199
- return path9.join(dir, `edge-${hash}.lock`);
53193
+ return path10.join(dir, `edge-${hash}.lock`);
53200
53194
  }
53201
53195
  function isEdgeProcess(pid) {
53202
53196
  try {
53203
53197
  process.kill(pid, 0);
53204
- const cmdline = fs12.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
53198
+ const cmdline = fs13.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
53205
53199
  return cmdline.includes("index.ts") || cmdline.includes("todoforai-edge");
53206
53200
  } catch {
53207
53201
  return false;
@@ -53209,7 +53203,7 @@ function isEdgeProcess(pid) {
53209
53203
  }
53210
53204
  function killExistingEdge(lp2) {
53211
53205
  try {
53212
- const pid = parseInt(fs12.readFileSync(lp2, "utf-8").trim(), 10);
53206
+ const pid = parseInt(fs13.readFileSync(lp2, "utf-8").trim(), 10);
53213
53207
  if (!isNaN(pid) && isEdgeProcess(pid)) {
53214
53208
  console.log(`\x1B[33mKilling existing edge process (pid ${pid})...\x1B[0m`);
53215
53209
  process.kill(pid, "SIGTERM");
@@ -53232,7 +53226,7 @@ function killExistingEdge(lp2) {
53232
53226
  }
53233
53227
  function acquireLock(lp2, kill = false) {
53234
53228
  try {
53235
- const pid = parseInt(fs12.readFileSync(lp2, "utf-8").trim(), 10);
53229
+ const pid = parseInt(fs13.readFileSync(lp2, "utf-8").trim(), 10);
53236
53230
  if (!isNaN(pid) && isEdgeProcess(pid)) {
53237
53231
  if (kill) {
53238
53232
  killExistingEdge(lp2);
@@ -53240,16 +53234,31 @@ function acquireLock(lp2, kill = false) {
53240
53234
  return false;
53241
53235
  }
53242
53236
  } catch {}
53243
- fs12.writeFileSync(lp2, String(process.pid));
53237
+ fs13.writeFileSync(lp2, String(process.pid));
53244
53238
  return true;
53245
53239
  }
53246
53240
  function releaseLock(lp2) {
53247
53241
  try {
53248
- if (fs12.readFileSync(lp2, "utf-8").trim() === String(process.pid))
53249
- fs12.unlinkSync(lp2);
53242
+ if (fs13.readFileSync(lp2, "utf-8").trim() === String(process.pid))
53243
+ fs13.unlinkSync(lp2);
53250
53244
  } catch {}
53251
53245
  }
53246
+ var MIN_BUN_VERSION = "1.3.14";
53247
+ function cmpSemver(a, b) {
53248
+ const pa2 = a.split(".").map(Number), pb2 = b.split(".").map(Number);
53249
+ for (let i10 = 0;i10 < 3; i10++)
53250
+ if ((pa2[i10] ?? 0) !== (pb2[i10] ?? 0))
53251
+ return (pa2[i10] ?? 0) - (pb2[i10] ?? 0);
53252
+ return 0;
53253
+ }
53252
53254
  async function main() {
53255
+ if (typeof Bun !== "undefined" && cmpSemver(Bun.version, MIN_BUN_VERSION) < 0) {
53256
+ console.error(`\x1B[31mBun ${Bun.version} is too old — WebSocket upgrade fails after fetch on the same host. Upgrade to >= ${MIN_BUN_VERSION} via \`bun upgrade\`.\x1B[0m`);
53257
+ process.exit(1);
53258
+ }
53259
+ const ownPkg = readOwnPackage();
53260
+ if (ownPkg)
53261
+ checkForUpdates(ownPkg);
53253
53262
  const config = await loadConfig();
53254
53263
  if (config.debug) {
53255
53264
  console.log("[config]", { apiUrl: config.apiUrl, debug: config.debug, addWorkspacePath: config.addWorkspacePath });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.13.10",
3
+ "version": "0.13.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"
@@ -16,14 +16,15 @@
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
18
  "dependencies": {
19
+ "@todoforai/update-notifier": "^0.1.0",
19
20
  "fflate": "^0.8.2",
20
21
  "ignore": "^7.0.5",
21
22
  "unpdf": "^1.4.0",
22
23
  "ws": "^8.18.0"
23
24
  },
24
25
  "devDependencies": {
25
- "@types/ws": "^8.5.12",
26
26
  "@types/node": "^22.0.0",
27
+ "@types/ws": "^8.5.12",
27
28
  "typescript": "^5.6.0"
28
29
  }
29
30
  }