@todoforai/edge 0.13.11 → 0.13.13

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 +78 -100
  2. package/package.json +2 -1
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
@@ -48488,12 +48477,14 @@ var BRIDGE_PORT = parseInt(process.env.TODOFORAI_BROWSER_BRIDGE_PORT || "43127",
48488
48477
  var BRIDGE_EDGE_ID = "local-browser-bridge";
48489
48478
  var REQUEST_TIMEOUT_MS = 30000;
48490
48479
  var isOpen = (ws) => !!ws && ws.readyState === import_websocket.default.OPEN;
48480
+ var describeExt = (ext) => ext ? `${ext.build ?? "?"}@${ext.version ?? "?"} (${ext.browser ?? "?"})` : "unknown extension";
48491
48481
 
48492
48482
  class BrowserExtensionBridge {
48493
48483
  debug;
48494
48484
  server;
48495
48485
  wss;
48496
48486
  extensionWs = null;
48487
+ extIdentity = null;
48497
48488
  pending = new Map;
48498
48489
  constructor(debug = false) {
48499
48490
  this.debug = debug;
@@ -48574,8 +48565,18 @@ class BrowserExtensionBridge {
48574
48565
  console.log("[browser-bridge:recv]", data.type);
48575
48566
  if (data.type === "hello") {
48576
48567
  const isTabChannel = data.role === "extension-tab" || data.role === "extension";
48577
- if (data.role === "extension-control")
48568
+ if (data.role === "extension-control") {
48569
+ const ident = describeExt(data.ext);
48570
+ const prev = this.extensionWs;
48571
+ if (isOpen(prev) && prev !== ws) {
48572
+ console.log(`[browser-bridge] control taken over by ${ident} (was ${describeExt(this.extIdentity)})`);
48573
+ prev.send(JSON.stringify({ type: "control_superseded", payload: { by: ident } }));
48574
+ } else if (this.debug) {
48575
+ console.log(`[browser-bridge] control acquired by ${ident}`);
48576
+ }
48578
48577
  this.extensionWs = ws;
48578
+ this.extIdentity = data.ext ?? null;
48579
+ }
48579
48580
  if (data.role === "extension-control" || isTabChannel) {
48580
48581
  if (isOpen(ws))
48581
48582
  ws.send(JSON.stringify({ type: "connected_edge", payload: { edgeId: BRIDGE_EDGE_ID } }));
@@ -48628,8 +48629,12 @@ class BrowserExtensionBridge {
48628
48629
  }
48629
48630
  }
48630
48631
  handleClose(ws) {
48631
- if (this.extensionWs === ws)
48632
+ if (this.extensionWs === ws) {
48633
+ if (this.debug)
48634
+ console.log(`[browser-bridge] control released by ${describeExt(this.extIdentity)}`);
48632
48635
  this.extensionWs = null;
48636
+ this.extIdentity = null;
48637
+ }
48633
48638
  for (const [requestId, pending] of this.pending) {
48634
48639
  if (pending.ws !== ws)
48635
48640
  continue;
@@ -48860,6 +48865,15 @@ var tool_catalog_default = {
48860
48865
  description: "Use to generate speech audio from text, clone voices, or list voices. Needs ELEVENLABS_API_KEY.",
48861
48866
  versionCmd: "elevenlabs-api --version 2>/dev/null | head -1"
48862
48867
  },
48868
+ "codex-imagegen-api": {
48869
+ category: "media",
48870
+ pkg: "@todoforai/codex-imagegen-api",
48871
+ installer: "npm",
48872
+ label: "Image Gen",
48873
+ capabilities: "AI image generation & editing via TODOFORAI backend (gpt-image)",
48874
+ description: 'Use to generate or edit images. Subcommands: `codex-imagegen-api generate "<prompt>" -o out.png [--size 1024x1024] [--quality low|medium|high]` and `codex-imagegen-api edit "<instruction>" -i in.png -o out.png` (repeat `-i` for multiple reference images).',
48875
+ versionCmd: "codex-imagegen-api --version 2>/dev/null | head -1"
48876
+ },
48863
48877
  "suno-api": {
48864
48878
  category: "media",
48865
48879
  pkg: "@todoforai/suno-api",
@@ -48940,7 +48954,7 @@ var tool_catalog_default = {
48940
48954
  pkg: "netlify-cli",
48941
48955
  installer: "npm",
48942
48956
  label: "Netlify",
48943
- statusCmd: `test -f ~/.netlify/config.json && netlify api getCurrentUser 2>&1 | grep -oP '"email":\\s*"\\K[^"]+' | head -1`,
48957
+ statusCmd: `netlify api getCurrentUser 2>&1 | grep -oP '"email":\\s*"\\K[^"]+' | head -1`,
48944
48958
  loginCmd: "netlify login",
48945
48959
  credentialPaths: [
48946
48960
  "~/.netlify/config.json"
@@ -48954,7 +48968,7 @@ var tool_catalog_default = {
48954
48968
  pkg: "firebase-tools",
48955
48969
  installer: "npm",
48956
48970
  label: "Firebase",
48957
- statusCmd: "test -f ~/.config/configstore/firebase-tools.json && firebase login:list 2>&1 | grep '@'",
48971
+ statusCmd: "firebase login:list 2>&1 | grep '@'",
48958
48972
  loginCmd: "firebase login",
48959
48973
  credentialPaths: [
48960
48974
  "~/.config/configstore/firebase-tools.json"
@@ -48968,7 +48982,7 @@ var tool_catalog_default = {
48968
48982
  pkg: "wrangler",
48969
48983
  installer: "npm",
48970
48984
  label: "Cloudflare",
48971
- statusCmd: "test -f ~/.config/.wrangler/config/default.toml && wrangler whoami 2>&1 | grep -v 'Failed' | grep '@'",
48985
+ statusCmd: "wrangler whoami 2>&1 | grep -v 'Failed' | grep '@'",
48972
48986
  loginCmd: "wrangler login",
48973
48987
  credentialPaths: [
48974
48988
  "~/.config/.wrangler/config/default.toml"
@@ -49010,7 +49024,7 @@ var tool_catalog_default = {
49010
49024
  pkg: "supabase",
49011
49025
  installer: "binary",
49012
49026
  label: "Supabase",
49013
- statusCmd: "test -f ~/.config/supabase/access-token && echo 'authenticated'",
49027
+ statusCmd: "supabase projects list >/dev/null 2>&1 && echo authenticated",
49014
49028
  loginCmd: "supabase login",
49015
49029
  credentialPaths: [
49016
49030
  "~/.config/supabase/access-token"
@@ -49024,7 +49038,7 @@ var tool_catalog_default = {
49024
49038
  pkg: "@railway/cli",
49025
49039
  installer: "npm",
49026
49040
  label: "Railway",
49027
- statusCmd: "test -f ~/.railway/config.json && railway whoami 2>&1 | grep -v 'Unauthorized'",
49041
+ statusCmd: "railway whoami 2>&1 | grep -v 'Unauthorized'",
49028
49042
  loginCmd: "railway login",
49029
49043
  credentialPaths: [
49030
49044
  "~/.railway/config.json"
@@ -49061,7 +49075,7 @@ var tool_catalog_default = {
49061
49075
  pkg: "@sentry/cli",
49062
49076
  installer: "npm",
49063
49077
  label: "Sentry",
49064
- statusCmd: "test -f ~/.sentryclirc && echo 'authenticated'",
49078
+ statusCmd: "sentry-cli info 2>&1 | grep -q 'Authentication Info' && echo authenticated",
49065
49079
  loginCmd: "sentry-cli login",
49066
49080
  credentialPaths: [
49067
49081
  "~/.sentryclirc"
@@ -49077,7 +49091,7 @@ var tool_catalog_default = {
49077
49091
  label: "TODOforAI",
49078
49092
  capabilities: "Create & manage TODOs, run workflows, API access",
49079
49093
  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",
49094
+ statusCmd: "todoai whoami >/dev/null 2>&1",
49081
49095
  installCmd: "bun add -g @todoforai/cli",
49082
49096
  versionCmd: "todoai --version 2>/dev/null | head -1",
49083
49097
  internal: true
@@ -49087,7 +49101,6 @@ var tool_catalog_default = {
49087
49101
  pkg: "newman",
49088
49102
  installer: "npm",
49089
49103
  label: "Postman",
49090
- statusCmd: "newman --version",
49091
49104
  capabilities: "Run API test collections, CI/CD integration, HTML reports",
49092
49105
  description: "Use to run Postman collections from CLI (API smoke tests, CI checks). `newman run <collection.json> -e <env.json>`.",
49093
49106
  versionCmd: "newman --version 2>/dev/null | head -1"
@@ -49098,7 +49111,6 @@ var tool_catalog_default = {
49098
49111
  installer: "system",
49099
49112
  label: "Web Request",
49100
49113
  binName: "curl",
49101
- statusCmd: "curl --version >/dev/null 2>&1",
49102
49114
  capabilities: "HTTP/HTTPS/FTP client: GET/POST/PUT/DELETE, headers, auth, file upload/download, follow redirects, cookies, TLS.",
49103
49115
  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
49116
  versionCmd: "curl --version 2>/dev/null | head -1",
@@ -49110,7 +49122,6 @@ var tool_catalog_default = {
49110
49122
  pkg: "cloudflared",
49111
49123
  installer: "binary",
49112
49124
  label: "Tunnel",
49113
- statusCmd: "cloudflared --version",
49114
49125
  capabilities: "Tunnel to localhost, expose local services, zero-trust access",
49115
49126
  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
49127
  versionCmd: "cloudflared --version 2>/dev/null | head -1"
@@ -49121,13 +49132,13 @@ var tool_catalog_default = {
49121
49132
  installer: "binary",
49122
49133
  preinstallCloud: true,
49123
49134
  label: "HashiCorp Vault",
49124
- statusCmd: "test -f ~/.vault-token && echo 'authenticated'",
49135
+ statusCmd: "vault token lookup >/dev/null 2>&1 && echo authenticated",
49125
49136
  loginCmd: "vault login",
49126
49137
  credentialPaths: [
49127
49138
  "~/.vault-token"
49128
49139
  ],
49129
49140
  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.",
49141
+ 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
49142
  versionCmd: "vault --version 2>/dev/null | head -1"
49132
49143
  },
49133
49144
  rclone: {
@@ -49136,16 +49147,17 @@ var tool_catalog_default = {
49136
49147
  installer: "system",
49137
49148
  preinstallCloud: true,
49138
49149
  label: "Cloud Sync",
49139
- statusCmd: "rclone listremotes 2>&1 | grep -v 'NOTICE' | head -5",
49150
+ statusCmd: "rclone listremotes 2>&1 | grep -v 'NOTICE' | grep -q ':' && echo authenticated",
49140
49151
  loginCmd: "rclone config",
49141
49152
  credentialPaths: [
49142
49153
  "~/.config/rclone/rclone.conf"
49143
49154
  ],
49144
49155
  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.",
49156
+ 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
49157
  subProviders: {
49147
49158
  gdrive: {
49148
49159
  label: "Google Drive",
49160
+ authType: "oauth",
49149
49161
  connectCmd: "rclone config create {{REMOTE}} drive",
49150
49162
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49151
49163
  remoteName: "gdrive",
@@ -49154,6 +49166,7 @@ var tool_catalog_default = {
49154
49166
  },
49155
49167
  onedrive: {
49156
49168
  label: "OneDrive",
49169
+ authType: "oauth",
49157
49170
  connectCmd: "rclone config create {{REMOTE}} onedrive",
49158
49171
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49159
49172
  remoteName: "onedrive",
@@ -49162,6 +49175,7 @@ var tool_catalog_default = {
49162
49175
  },
49163
49176
  dropbox: {
49164
49177
  label: "Dropbox",
49178
+ authType: "oauth",
49165
49179
  connectCmd: "rclone config create {{REMOTE}} dropbox",
49166
49180
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49167
49181
  remoteName: "dropbox",
@@ -49170,6 +49184,7 @@ var tool_catalog_default = {
49170
49184
  },
49171
49185
  s3: {
49172
49186
  label: "Amazon S3",
49187
+ authType: "credentials",
49173
49188
  connectCmd: "rclone config create {{REMOTE}} s3",
49174
49189
  statusCmd: "rclone lsd {{REMOTE}}: 2>&1 | head -1",
49175
49190
  remoteName: "s3",
@@ -49183,7 +49198,6 @@ var tool_catalog_default = {
49183
49198
  pkg: "pymupdf",
49184
49199
  installer: "pip",
49185
49200
  label: "PDF",
49186
- statusCmd: "python3 -c 'import pymupdf' 2>/dev/null || /usr/bin/python3 -c 'import pymupdf' 2>/dev/null",
49187
49201
  capabilities: "PDF text editing, extraction, merge, split",
49188
49202
  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
49203
  versionCmd: "python3 -c 'import pymupdf; print(pymupdf.__version__)' 2>/dev/null",
@@ -49195,9 +49209,8 @@ var tool_catalog_default = {
49195
49209
  installer: "npm",
49196
49210
  preinstallCloud: true,
49197
49211
  label: "Browser",
49198
- statusCmd: "agent-browser --version",
49199
49212
  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>.",
49213
+ 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
49214
  versionCmd: "agent-browser --version 2>/dev/null | head -1"
49202
49215
  },
49203
49216
  firecrawl: {
@@ -49220,7 +49233,7 @@ var tool_catalog_default = {
49220
49233
  installer: "npm",
49221
49234
  label: "Browser (Extension)",
49222
49235
  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.",
49236
+ 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
49237
  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
49238
  internal: true
49226
49239
  },
@@ -49254,6 +49267,7 @@ var tool_catalog_default = {
49254
49267
  capabilities: "Explore a codebase as a real TODO: read-only agent maps structure, surfaces relevant files, streams findings to terminal",
49255
49268
  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
49269
  versionCmd: "tfa-explore --version 2>/dev/null | head -1",
49270
+ statusCmd: "tfa-explore whoami",
49257
49271
  installCmd: "bun add -g @todoforai/tfa-explore",
49258
49272
  preinstall: true,
49259
49273
  preinstallCloud: true,
@@ -49267,6 +49281,7 @@ var tool_catalog_default = {
49267
49281
  capabilities: "Review a git diff as a real TODO: read-only agent assesses goal, finds issues, suggests simpler approaches",
49268
49282
  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
49283
  versionCmd: "tfa-review --version 2>/dev/null | head -1",
49284
+ statusCmd: "tfa-review whoami",
49270
49285
  installCmd: "bun add -g @todoforai/tfa-review",
49271
49286
  preinstall: true,
49272
49287
  preinstallCloud: true,
@@ -49280,6 +49295,7 @@ var tool_catalog_default = {
49280
49295
  capabilities: "Summarize files or piped input as a real TODO with no-tools sub-agent",
49281
49296
  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
49297
  versionCmd: "tfa-summary --version 2>/dev/null | head -1",
49298
+ statusCmd: "tfa-summary whoami",
49283
49299
  installCmd: "bun add -g @todoforai/tfa-summary",
49284
49300
  internal: true
49285
49301
  },
@@ -49289,13 +49305,12 @@ var tool_catalog_default = {
49289
49305
  installer: "npm",
49290
49306
  label: "Vault",
49291
49307
  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.',
49308
+ 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
49309
  versionCmd: "tfa-vault --version 2>/dev/null | head -1",
49294
49310
  statusCmd: "tfa-vault whoami",
49295
49311
  installCmd: "bun add -g @todoforai/vault",
49296
49312
  preinstall: true,
49297
- preinstallCloud: true,
49298
- internal: true
49313
+ preinstallCloud: true
49299
49314
  },
49300
49315
  ripgrep: {
49301
49316
  category: "development",
@@ -49531,17 +49546,6 @@ function isToolInstalled(name) {
49531
49546
  }
49532
49547
  return whichWithTools(name) !== null;
49533
49548
  }
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
49549
  async function installBinary(name) {
49546
49550
  const urlFunc = BINARY_URL_FUNCS[name];
49547
49551
  if (!urlFunc) {
@@ -51592,7 +51596,6 @@ ${this.lastPart}`;
51592
51596
  var processes = new Map;
51593
51597
  var outputBuffers = new Map;
51594
51598
  var completionResolvers = new Map;
51595
- var pendingToolApprovals = new Map;
51596
51599
  var exitedOutputByPid = new Map;
51597
51600
  async function executeBlock(blockId, content, send, todoId, messageId, timeout, cwd, manual = false, runMode, edgeId, agentSettingsId = "", keepAliveOnTimeout = false) {
51598
51601
  if (processes.has(blockId)) {
@@ -51611,38 +51614,6 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51611
51614
  } else {
51612
51615
  cwd = tmpDir;
51613
51616
  }
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
51617
  await send({
51647
51618
  type: "BLOCK_UPDATE",
51648
51619
  payload: { todoId, blockId, messageId, updates: { status: "RUNNING" } }
@@ -51931,20 +51902,13 @@ var MAX_DIRS_PER_ROOT = 2000;
51931
51902
  var FRONTMATTER_BYTES = 8 * 1024;
51932
51903
  var MAX_NAME_LEN = 64;
51933
51904
  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
51905
  async function discoverSkills(rootPaths, opts = {}) {
51941
51906
  const includeUserScope = opts.includeUserScope ?? true;
51942
51907
  const roots = [
51943
- ...rootPaths.map((p10) => ({ path: path6.join(p10, ".agents", "skills"), relTo: p10, scope: "repo" }))
51908
+ ...rootPaths.map((p10) => ({ path: path6.join(p10, ".agents", "skills"), scope: "repo" }))
51944
51909
  ];
51945
51910
  if (includeUserScope) {
51946
- const home = os7.homedir();
51947
- roots.push({ path: path6.join(home, ".agents", "skills"), relTo: home, scope: "user" });
51911
+ roots.push({ path: path6.join(os7.homedir(), ".agents", "skills"), scope: "user" });
51948
51912
  }
51949
51913
  const skills = [];
51950
51914
  const errors = [];
@@ -51962,11 +51926,11 @@ async function discoverSkills(rootPaths, opts = {}) {
51962
51926
  }
51963
51927
  if (!stat.isDirectory())
51964
51928
  continue;
51965
- walkRoot(root.path, root.relTo, root.scope, skills, errors, seenSkillPaths);
51929
+ walkRoot(root.path, root.scope, skills, errors, seenSkillPaths);
51966
51930
  }
51967
51931
  return { skills, errors };
51968
51932
  }
51969
- function walkRoot(root, relTo, scope, skills, errors, seen) {
51933
+ function walkRoot(root, scope, skills, errors, seen) {
51970
51934
  const queue = [{ dir: root, depth: 0 }];
51971
51935
  let dirsVisited = 0;
51972
51936
  for (let i10 = 0;i10 < queue.length; i10++) {
@@ -52002,14 +51966,14 @@ function walkRoot(root, relTo, scope, skills, errors, seen) {
52002
51966
  if (seen.has(full))
52003
51967
  continue;
52004
51968
  seen.add(full);
52005
- const skill = parseSkillFile(full, relTo, scope, errors);
51969
+ const skill = parseSkillFile(full, scope, errors);
52006
51970
  if (skill)
52007
51971
  skills.push(skill);
52008
51972
  }
52009
51973
  }
52010
51974
  }
52011
51975
  }
52012
- function parseSkillFile(filePath, relTo, scope, errors) {
51976
+ function parseSkillFile(filePath, scope, errors) {
52013
51977
  let head;
52014
51978
  try {
52015
51979
  const fd3 = fs9.openSync(filePath, "r");
@@ -52042,7 +52006,7 @@ function parseSkillFile(filePath, relTo, scope, errors) {
52042
52006
  name,
52043
52007
  description,
52044
52008
  shortDescription: shortDescription && shortDescription.length <= MAX_DESC_LEN ? shortDescription : undefined,
52045
- path: shortestPath(filePath, relTo),
52009
+ path: filePath,
52046
52010
  scope
52047
52011
  };
52048
52012
  }
@@ -52372,9 +52336,6 @@ register("execute_shell_command", async (args, client) => {
52372
52336
  try {
52373
52337
  await send(msg.shellBlockStart(todoId, blockId, "execute", messageId));
52374
52338
  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
52339
  await waitForCompletion(blockId, (timeout + 5) * 1000);
52379
52340
  const rawOutput = getBlockRawOutput(blockId);
52380
52341
  let output = rawOutput ?? getBlockOutput(blockId);
@@ -52686,8 +52647,6 @@ async function handleFunctionCall(payload, send, client) {
52686
52647
  throw new Error(`Unknown function: ${functionName}. Available: ${available.join(", ")}`);
52687
52648
  }
52688
52649
  const result = await fn2(args, client);
52689
- if (result && result.__awaiting_approval__)
52690
- return;
52691
52650
  await send(makeSuccess(result));
52692
52651
  } catch (e) {
52693
52652
  log3("error", `Function call '${functionName}' failed:`, e.message);
@@ -53186,10 +53145,17 @@ class ServerError extends Error {
53186
53145
  }
53187
53146
  }
53188
53147
 
53189
- // src/update-notifier.ts
53148
+ // node_modules/@todoforai/update-notifier/src/index.ts
53190
53149
  import fs12 from "node:fs";
53191
53150
  import path9 from "node:path";
53192
53151
  import os9 from "node:os";
53152
+ function isLinkedInstall() {
53153
+ try {
53154
+ return !fs12.realpathSync(process.argv[1] || "").includes(`${path9.sep}node_modules${path9.sep}`);
53155
+ } catch {
53156
+ return false;
53157
+ }
53158
+ }
53193
53159
  var TTL_MS = 24 * 60 * 60 * 1000;
53194
53160
  var CACHE_DIR = path9.join(os9.homedir(), ".config", "todoforai");
53195
53161
  function cmpVer(a, b) {
@@ -53203,7 +53169,7 @@ function cmpVer(a, b) {
53203
53169
  return 0;
53204
53170
  }
53205
53171
  function checkForUpdates(pkg) {
53206
- if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER)
53172
+ if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER || isLinkedInstall())
53207
53173
  return;
53208
53174
  const cacheFile = path9.join(CACHE_DIR, `notifier-${encodeURIComponent(pkg.name)}.json`);
53209
53175
  let cache = {};
@@ -53302,7 +53268,19 @@ function releaseLock(lp2) {
53302
53268
  fs13.unlinkSync(lp2);
53303
53269
  } catch {}
53304
53270
  }
53271
+ var MIN_BUN_VERSION = "1.3.14";
53272
+ function cmpSemver(a, b) {
53273
+ const pa2 = a.split(".").map(Number), pb2 = b.split(".").map(Number);
53274
+ for (let i10 = 0;i10 < 3; i10++)
53275
+ if ((pa2[i10] ?? 0) !== (pb2[i10] ?? 0))
53276
+ return (pa2[i10] ?? 0) - (pb2[i10] ?? 0);
53277
+ return 0;
53278
+ }
53305
53279
  async function main() {
53280
+ if (typeof Bun !== "undefined" && cmpSemver(Bun.version, MIN_BUN_VERSION) < 0) {
53281
+ 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`);
53282
+ process.exit(1);
53283
+ }
53306
53284
  const ownPkg = readOwnPackage();
53307
53285
  if (ownPkg)
53308
53286
  checkForUpdates(ownPkg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.13.11",
3
+ "version": "0.13.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"
@@ -16,6 +16,7 @@
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",