@todoforai/edge 0.13.18 → 0.13.20

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 +275 -210
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -47977,10 +47977,10 @@ function setConnectionContext(get) {
47977
47977
  function getConnectionEnv() {
47978
47978
  if (!getter)
47979
47979
  return {};
47980
- const { apiUrl, apiKey } = getter();
47981
- if (!apiUrl || !apiKey)
47980
+ const { apiUrl, sessionToken } = getter();
47981
+ if (!apiUrl || !sessionToken)
47982
47982
  return {};
47983
- return { TODOFORAI_API_URL: apiUrl, TODOFORAI_API_TOKEN: apiKey };
47983
+ return { TODOFORAI_API_URL: apiUrl, TODOFORAI_API_TOKEN: sessionToken };
47984
47984
  }
47985
47985
 
47986
47986
  // src/constants.ts
@@ -48033,7 +48033,8 @@ var EF = {
48033
48033
  FRONTEND_FILE_CHUNK_RESULT: "frontend:file_chunk_result"
48034
48034
  };
48035
48035
  var S2E = {
48036
- EDGE_CONFIG_UPDATE: "edge:config_update"
48036
+ EDGE_CONFIG_UPDATE: "edge:config_update",
48037
+ SESSION_TOKEN: "edge:session_token"
48037
48038
  };
48038
48039
  var msg = {
48039
48040
  edgeStatus(edgeId, status) {
@@ -48144,7 +48145,7 @@ class ApiClient {
48144
48145
  }
48145
48146
  async request(method, endpoint, body) {
48146
48147
  const url = `${this.apiUrl}${endpoint}`;
48147
- const opts = { method, headers: this.headers };
48148
+ const opts = { method, headers: this.headers, signal: AbortSignal.timeout(30000) };
48148
48149
  if (body)
48149
48150
  opts.body = JSON.stringify(body);
48150
48151
  const res = await fetch(url, opts);
@@ -48771,7 +48772,6 @@ var tool_catalog_default = {
48771
48772
  "~/.zele/sqlite.db"
48772
48773
  ],
48773
48774
  capabilities: "Read inbox, search/send/reply email, email addresses — multi-account Gmail, OAuth browser login for setup.",
48774
- description: "Use for anything Gmail: reading the user's inbox, searching mail, sending or replying to email. Multi-account, OAuth login via `zele login`.",
48775
48775
  versionCmd: "zele --version 2>/dev/null | head -1"
48776
48776
  },
48777
48777
  xurl: {
@@ -48785,7 +48785,7 @@ var tool_catalog_default = {
48785
48785
  "~/.xurl"
48786
48786
  ],
48787
48787
  capabilities: "Post tweets, reply & quote, read timelines, search tweets, like & repost, follow/unfollow, DMs, media uploads",
48788
- description: "Use to interact with X/Twitter: post/reply/quote tweets, search, read timelines, like/repost, follow, DMs, media upload. Authenticated raw API access via `xurl <method> <path>`.",
48788
+ description: "Authenticated raw X API access via `xurl <method> <path>`; login `xurl auth oauth2`.",
48789
48789
  versionCmd: "xurl --version 2>/dev/null | head -1"
48790
48790
  },
48791
48791
  "tiktok-uploader": {
@@ -48798,7 +48798,6 @@ var tool_catalog_default = {
48798
48798
  "~/.tiktok/cookies.txt"
48799
48799
  ],
48800
48800
  capabilities: "Upload videos, batch uploads, schedule posts, custom covers, hashtags & mentions",
48801
- description: "Upload videos to TikTok, schedule posts, set covers and hashtags.",
48802
48801
  versionCmd: "pip show tiktok-uploader 2>/dev/null | grep -oP 'Version: \\K.*'"
48803
48802
  },
48804
48803
  instagrapi: {
@@ -48807,7 +48806,6 @@ var tool_catalog_default = {
48807
48806
  installer: "pip",
48808
48807
  label: "Instagram",
48809
48808
  capabilities: "Upload photos & reels, post stories, send DMs, like & comment, manage followers",
48810
- description: "Instagram automation: upload photos/reels, stories, DMs, likes, follower management.",
48811
48809
  versionCmd: "pip show instagrapi 2>/dev/null | grep -oP 'Version: \\K.*'"
48812
48810
  },
48813
48811
  mudslide: {
@@ -48821,9 +48819,50 @@ var tool_catalog_default = {
48821
48819
  "~/.local/share/mudslide"
48822
48820
  ],
48823
48821
  capabilities: "Send messages, send images & files, send locations & polls, group management, QR code login",
48824
- description: "Use to send WhatsApp messages/files/media/locations/polls from CLI. QR-code login via `mudslide login`.",
48825
48822
  versionCmd: "mudslide --version 2>/dev/null | head -1"
48826
48823
  },
48824
+ slack: {
48825
+ category: "development",
48826
+ pkg: "slack-cli",
48827
+ installer: "binary",
48828
+ label: "Slack",
48829
+ statusCmd: "slack-cli auth list 2>&1 | grep -q 'Team' && echo authenticated",
48830
+ loginCmd: "slack-cli login",
48831
+ credentialPaths: [
48832
+ "~/.slack/credentials.json"
48833
+ ],
48834
+ capabilities: "Build & deploy Slack apps, manage workspaces & triggers, run datastore queries, tail logs — official Slack CLI.",
48835
+ description: "`slack-cli login` to auth. Invoked as `slack-cli` to avoid colliding with the Slack desktop app's `slack` binary.",
48836
+ versionCmd: "slack-cli version 2>/dev/null | head -1",
48837
+ binName: "slack-cli",
48838
+ binary: {
48839
+ "linux-x86_64": {
48840
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_amd64.tar.gz",
48841
+ archive: "tar.gz",
48842
+ extract: "bin/slack"
48843
+ },
48844
+ "linux-aarch64": {
48845
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_arm64.tar.gz",
48846
+ archive: "tar.gz",
48847
+ extract: "bin/slack"
48848
+ },
48849
+ "darwin-x86_64": {
48850
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_amd64.tar.gz",
48851
+ archive: "tar.gz",
48852
+ extract: "bin/slack"
48853
+ },
48854
+ "darwin-aarch64": {
48855
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_arm64.tar.gz",
48856
+ archive: "tar.gz",
48857
+ extract: "bin/slack"
48858
+ },
48859
+ "windows-x86_64": {
48860
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_windows_64-bit.zip",
48861
+ archive: "zip",
48862
+ extract: "bin/slack.exe"
48863
+ }
48864
+ }
48865
+ },
48827
48866
  "telegram-send": {
48828
48867
  category: "messaging",
48829
48868
  pkg: "telegram-send",
@@ -48834,18 +48873,8 @@ var tool_catalog_default = {
48834
48873
  "~/.config/telegram-send/telegram-send.conf"
48835
48874
  ],
48836
48875
  capabilities: "Send messages, send files & images, send video & audio, Markdown/HTML formatting, channel & group support",
48837
- description: "Send Telegram messages and files from CLI via a bot.",
48838
48876
  versionCmd: "telegram-send --version 2>/dev/null | head -1"
48839
48877
  },
48840
- "apollo-api": {
48841
- category: "marketing",
48842
- pkg: "@todoforai/apollo-api",
48843
- installer: "npm",
48844
- label: "Apollo",
48845
- capabilities: "Lead search & enrichment, email sequences, CRM sync",
48846
- description: "Use for B2B prospecting: search Apollo's people/company DB, enrich leads with emails/titles/companies, manage sequences and CRM data. Needs APOLLO_API_KEY.",
48847
- versionCmd: "apollo-api --version 2>/dev/null | head -1"
48848
- },
48849
48878
  "meta-ads": {
48850
48879
  category: "marketing",
48851
48880
  pkg: "meta-ads",
@@ -48853,7 +48882,6 @@ var tool_catalog_default = {
48853
48882
  label: "Meta Ads",
48854
48883
  statusCmd: "meta auth status 2>&1",
48855
48884
  capabilities: "Meta/Facebook & Instagram ad campaigns: campaigns/adsets/ads CRUD, insights & reporting, catalog/product-feed/product-set/product-item, ad creatives, datasets (pixels), pages — Marketing API via the official `meta` CLI.",
48856
- description: "Manage Meta/Facebook & Instagram ad campaigns, insights, creatives, and catalogs.",
48857
48885
  versionCmd: "meta --version 2>/dev/null | head -1"
48858
48886
  },
48859
48887
  "elevenlabs-api": {
@@ -48861,8 +48889,7 @@ var tool_catalog_default = {
48861
48889
  pkg: "@todoforai/elevenlabs-api",
48862
48890
  installer: "npm",
48863
48891
  label: "ElevenLabs",
48864
- capabilities: "Text-to-speech, voice cloning, multiple languages",
48865
- description: "Use to generate speech audio from text, clone voices, or list voices. Needs ELEVENLABS_API_KEY.",
48892
+ capabilities: "Text-to-speech, voice cloning, multiple languages. Needs ELEVENLABS_API_KEY.",
48866
48893
  versionCmd: "elevenlabs-api --version 2>/dev/null | head -1"
48867
48894
  },
48868
48895
  "codex-imagegen-api": {
@@ -48871,7 +48898,6 @@ var tool_catalog_default = {
48871
48898
  installer: "npm",
48872
48899
  label: "Image Gen",
48873
48900
  capabilities: "AI image generation & editing via TODOFORAI backend (gpt-image)",
48874
- description: "Generate or edit images with AI.",
48875
48901
  versionCmd: "codex-imagegen-api --version 2>/dev/null | head -1"
48876
48902
  },
48877
48903
  "suno-api": {
@@ -48879,8 +48905,7 @@ var tool_catalog_default = {
48879
48905
  pkg: "@todoforai/suno-api",
48880
48906
  installer: "npm",
48881
48907
  label: "Suno",
48882
- capabilities: "AI music generation, custom lyrics, multiple genres",
48883
- description: "Use to generate music/songs from a prompt or custom lyrics. Needs SUNO_API_KEY.",
48908
+ capabilities: "AI music generation, custom lyrics, multiple genres. Needs SUNO_API_KEY.",
48884
48909
  versionCmd: "suno-api --version 2>/dev/null | head -1"
48885
48910
  },
48886
48911
  ntn: {
@@ -48894,7 +48919,6 @@ var tool_catalog_default = {
48894
48919
  "~/.config/notion"
48895
48920
  ],
48896
48921
  capabilities: "Official Notion CLI: workspace-wide OAuth login, raw API calls (`ntn api <path>`), page/datasource management, file uploads, Workers deploy.",
48897
- description: "Read, create, and update Notion pages and databases.",
48898
48922
  versionCmd: "ntn --version 2>/dev/null | head -1"
48899
48923
  },
48900
48924
  "perplexity-api": {
@@ -48903,7 +48927,6 @@ var tool_catalog_default = {
48903
48927
  installer: "npm",
48904
48928
  label: "Perplexity",
48905
48929
  capabilities: "AI-powered web search, chat completions, async chat, embeddings and agent runs",
48906
- description: "Web-grounded AI search with citations. Use when you need fresh information beyond training cutoff.",
48907
48930
  versionCmd: "perplexity-api --version 2>/dev/null | head -1"
48908
48931
  },
48909
48932
  gh: {
@@ -48918,7 +48941,7 @@ var tool_catalog_default = {
48918
48941
  "~/.config/gh/hosts.yml"
48919
48942
  ],
48920
48943
  capabilities: "Repos, PRs & issues, actions & releases, gists & SSH keys",
48921
- description: "Use for any GitHub operation: create/list/merge PRs, file issues, manage releases, trigger/inspect Actions, clone/fork repos, gists. Prefer `gh` over raw GitHub API calls.",
48944
+ description: "Prefer `gh` over raw GitHub API calls.",
48922
48945
  versionCmd: "gh --version 2>/dev/null | head -1"
48923
48946
  },
48924
48947
  glab: {
@@ -48932,7 +48955,7 @@ var tool_catalog_default = {
48932
48955
  "~/.config/glab-cli/config.yml"
48933
48956
  ],
48934
48957
  capabilities: "Repos, MRs & issues, CI/CD pipelines, releases",
48935
- description: "Use for any GitLab operation: MRs, issues, pipelines, releases, repo management. GitLab equivalent of `gh`.",
48958
+ description: "GitLab equivalent of `gh`.",
48936
48959
  versionCmd: "glab --version 2>/dev/null | head -1"
48937
48960
  },
48938
48961
  vercel: {
@@ -48946,7 +48969,7 @@ var tool_catalog_default = {
48946
48969
  "~/.local/share/com.vercel.cli/auth.json"
48947
48970
  ],
48948
48971
  capabilities: "Deploy & preview, environment variables, domain management",
48949
- description: "Use to deploy a project to Vercel (`vercel deploy`), manage env vars, domains, and preview URLs.",
48972
+ description: "`vercel deploy`; manage env vars, domains, preview URLs.",
48950
48973
  versionCmd: "vercel --version 2>/dev/null | head -1"
48951
48974
  },
48952
48975
  netlify: {
@@ -48960,7 +48983,7 @@ var tool_catalog_default = {
48960
48983
  "~/.netlify/config.json"
48961
48984
  ],
48962
48985
  capabilities: "Deploy & preview, serverless functions, forms & identity",
48963
- description: "Use to deploy sites/functions to Netlify, manage env vars, forms, identity. `netlify deploy --prod` for production.",
48986
+ description: "`netlify deploy --prod` for production; manage env vars, forms, identity.",
48964
48987
  versionCmd: "netlify --version 2>/dev/null | head -1"
48965
48988
  },
48966
48989
  firebase: {
@@ -48974,7 +48997,7 @@ var tool_catalog_default = {
48974
48997
  "~/.config/configstore/firebase-tools.json"
48975
48998
  ],
48976
48999
  capabilities: "Hosting & deploy, Firestore & Realtime DB, auth & functions",
48977
- description: "Use for Firebase: deploy hosting/functions, manage Firestore/Realtime DB rules and data, auth users, emulators.",
49000
+ description: "Deploy hosting/functions, manage Firestore/RTDB rules and data, auth users, emulators.",
48978
49001
  versionCmd: "firebase --version 2>/dev/null | head -1"
48979
49002
  },
48980
49003
  wrangler: {
@@ -48988,7 +49011,7 @@ var tool_catalog_default = {
48988
49011
  "~/.config/.wrangler/config/default.toml"
48989
49012
  ],
48990
49013
  capabilities: "Workers & Pages deploy, KV & D1 storage, R2 & Queues",
48991
- description: "Use for Cloudflare developer platform: deploy Workers/Pages, manage KV/D1/R2/Queues, tail logs. Use `cloudflared` for tunnels.",
49014
+ description: "Tail logs with `wrangler tail`. Use `cloudflared` for tunnels.",
48992
49015
  versionCmd: "wrangler --version 2>/dev/null | head -1"
48993
49016
  },
48994
49017
  stripe: {
@@ -49002,7 +49025,7 @@ var tool_catalog_default = {
49002
49025
  "~/.config/stripe/config.toml"
49003
49026
  ],
49004
49027
  capabilities: "Payments & subscriptions, webhook testing, invoice management",
49005
- description: "Use for Stripe: inspect/create customers, charges, subscriptions, invoices, webhooks. `stripe listen` to forward webhooks locally; `stripe get/post /v1/...` for raw API.",
49028
+ description: "`stripe listen` forwards webhooks locally; `stripe get/post /v1/...` for raw API.",
49006
49029
  versionCmd: "stripe --version 2>/dev/null | head -1"
49007
49030
  },
49008
49031
  flyctl: {
@@ -49016,7 +49039,7 @@ var tool_catalog_default = {
49016
49039
  "~/.fly/config.yml"
49017
49040
  ],
49018
49041
  capabilities: "Deploy containers globally, Postgres & volumes, auto-scaling",
49019
- description: "Use to deploy apps/containers to Fly.io edge, manage Postgres/volumes/machines/secrets. `flyctl deploy` from app dir.",
49042
+ description: "`flyctl deploy` from app dir; manage Postgres/volumes/machines/secrets.",
49020
49043
  versionCmd: "flyctl version 2>/dev/null | head -1"
49021
49044
  },
49022
49045
  supabase: {
@@ -49030,7 +49053,7 @@ var tool_catalog_default = {
49030
49053
  "~/.config/supabase/access-token"
49031
49054
  ],
49032
49055
  capabilities: "Postgres database, auth & storage, edge functions",
49033
- description: "Use for Supabase projects: run/migrate local Postgres, deploy edge functions, manage auth/storage, link to cloud project.",
49056
+ description: "Link to cloud project; runs/migrates local Postgres.",
49034
49057
  versionCmd: "supabase --version 2>/dev/null | head -1"
49035
49058
  },
49036
49059
  railway: {
@@ -49044,7 +49067,7 @@ var tool_catalog_default = {
49044
49067
  "~/.railway/config.json"
49045
49068
  ],
49046
49069
  capabilities: "Deploy apps & databases, environment management, auto-scaling",
49047
- description: "Use to deploy apps/services to Railway, provision databases, manage env vars and environments. `railway up` to deploy current dir.",
49070
+ description: "`railway up` to deploy current dir.",
49048
49071
  versionCmd: "railway --version 2>/dev/null | head -1"
49049
49072
  },
49050
49073
  shopify: {
@@ -49057,16 +49080,23 @@ var tool_catalog_default = {
49057
49080
  "~/.config/shopify/config.json"
49058
49081
  ],
49059
49082
  capabilities: "Theme development, app scaffolding, store management",
49060
- description: "Use for Shopify dev: scaffold/develop themes and apps, push/pull themes, run dev server against a store.",
49061
49083
  versionCmd: "shopify version 2>/dev/null | head -1"
49062
49084
  },
49085
+ "shop-app": {
49086
+ category: "ecommerce",
49087
+ pkg: "@todoforai/shop-app",
49088
+ installer: "npm",
49089
+ label: "Shop",
49090
+ capabilities: "Product search & price comparison across Shopify stores",
49091
+ description: "Shop.app product search across Shopify stores (no auth). `shop-app search '<query>' --ships-to HU -n 10` returns products with price, rating, URL, variants. `shop-app similar <variantId>` for lookalikes.",
49092
+ versionCmd: "shop-app --version 2>/dev/null | head -1"
49093
+ },
49063
49094
  "datadog-ci": {
49064
49095
  category: "monitoring",
49065
49096
  pkg: "@datadog/datadog-ci",
49066
49097
  installer: "npm",
49067
49098
  label: "Datadog",
49068
- capabilities: "CI test visibility, sourcemap uploads, deployment tracking",
49069
- description: "Use in CI to upload test results/sourcemaps/coverage to Datadog and mark deployments. Needs DATADOG_API_KEY.",
49099
+ capabilities: "CI test visibility, sourcemap uploads, deployment tracking. Needs DATADOG_API_KEY.",
49070
49100
  versionCmd: "datadog-ci version 2>/dev/null | head -1"
49071
49101
  },
49072
49102
  "sentry-cli": {
@@ -49080,7 +49110,7 @@ var tool_catalog_default = {
49080
49110
  "~/.sentryclirc"
49081
49111
  ],
49082
49112
  capabilities: "Release management, sourcemap uploads, error monitoring",
49083
- description: "Use to create Sentry releases, upload sourcemaps/debug-symbols, associate commits, send events. `sentry-cli releases new <version>`.",
49113
+ description: "`sentry-cli releases new <version>`.",
49084
49114
  versionCmd: "sentry-cli --version 2>/dev/null | head -1"
49085
49115
  },
49086
49116
  todoai: {
@@ -49089,7 +49119,7 @@ var tool_catalog_default = {
49089
49119
  installer: "bun",
49090
49120
  label: "TODOforAI",
49091
49121
  capabilities: "Create/list/inspect/update TODOs, run templates & workflows, platform API access",
49092
- description: "Use for the TODOforAI platform — this is how you reach the user's OWN TODOs: `todoai list` (filter with `--status open`) to browse them, `todoai \"prompt\"` to create one, `todoai --inspect <id>` to read a TODO's full chat log, `todoai status/addmessage/delete` to manage them, `--template` to run a registry workflow. Never claim you lack access to platform TODOs — reach them here; run `--help` for details.",
49122
+ description: '`todoai list` (`--status open`) to browse, `todoai "prompt"` to create, `--inspect <id>` to read a chat log, `status/addmessage/delete` to manage, `--template` for registry workflows.',
49093
49123
  installCmd: "bun add -g @todoforai/cli",
49094
49124
  versionCmd: "todoai --version 2>/dev/null | head -1",
49095
49125
  internal: true
@@ -49100,7 +49130,7 @@ var tool_catalog_default = {
49100
49130
  installer: "npm",
49101
49131
  label: "Postman",
49102
49132
  capabilities: "Run API test collections, CI/CD integration, HTML reports",
49103
- description: "Use to run Postman collections from CLI (API smoke tests, CI checks). `newman run <collection.json> -e <env.json>`.",
49133
+ description: "`newman run <collection.json> -e <env.json>`.",
49104
49134
  versionCmd: "newman --version 2>/dev/null | head -1"
49105
49135
  },
49106
49136
  curl: {
@@ -49121,22 +49151,20 @@ var tool_catalog_default = {
49121
49151
  installer: "binary",
49122
49152
  label: "Tunnel",
49123
49153
  capabilities: "Tunnel to localhost, expose local services, zero-trust access",
49124
- 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.",
49154
+ description: "`cloudflared tunnel --url http://localhost:<port>` prints a public https URL.",
49125
49155
  versionCmd: "cloudflared --version 2>/dev/null | head -1"
49126
49156
  },
49127
49157
  vault: {
49128
49158
  category: "security",
49129
49159
  pkg: "vault",
49130
49160
  installer: "binary",
49131
- preinstallCloud: true,
49132
49161
  label: "HashiCorp Vault",
49133
49162
  statusCmd: "vault token lookup >/dev/null 2>&1 && echo authenticated",
49134
49163
  loginCmd: "vault login",
49135
49164
  credentialPaths: [
49136
49165
  "~/.vault-token"
49137
49166
  ],
49138
- capabilities: "Secrets management, dynamic credentials",
49139
- 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.",
49167
+ capabilities: "Secrets management, dynamic credentials. Needs VAULT_ADDR + VAULT_TOKEN; for credential read/write use `tfa-vault` instead.",
49140
49168
  versionCmd: "vault --version 2>/dev/null | head -1"
49141
49169
  },
49142
49170
  rclone: {
@@ -49151,7 +49179,6 @@ var tool_catalog_default = {
49151
49179
  "~/.config/rclone/rclone.conf"
49152
49180
  ],
49153
49181
  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).",
49154
- description: "Access cloud storage (Google Drive, OneDrive, Dropbox, S3, 40+ providers). List, copy, sync, or mount files.",
49155
49182
  subProviders: {
49156
49183
  gdrive: {
49157
49184
  label: "Google Drive",
@@ -49196,8 +49223,7 @@ var tool_catalog_default = {
49196
49223
  pkg: "pymupdf",
49197
49224
  installer: "pip",
49198
49225
  label: "PDF",
49199
- capabilities: "PDF text editing, extraction, merge, split",
49200
- description: "Read, edit, merge, or split PDFs programmatically. Call via `python3 -c`.",
49226
+ capabilities: "PDF text editing, extraction, merge, split. Call via `python3 -c`.",
49201
49227
  versionCmd: "python3 -c 'import pymupdf; print(pymupdf.__version__)' 2>/dev/null",
49202
49228
  preinstallCloud: true
49203
49229
  },
@@ -49208,16 +49234,16 @@ var tool_catalog_default = {
49208
49234
  preinstallCloud: true,
49209
49235
  label: "Browser",
49210
49236
  capabilities: "Headless browser automation, web scraping, accessibility tree snapshots, screenshots, PDF generation",
49211
- description: "Headless browser. Use for JS-rendered pages, scraping, screenshots, PDFs, and automated flows. Default browser prefer over `todoforai-browser` unless the user's own session is needed.",
49237
+ description: "Default browser. On a PC with a display prefer a visible window the user can watch/interact with (needed for CAPTCHA/MFA/login): launch a separate Chrome with `google-chrome --remote-debugging-port=9222 --user-data-dir=$HOME/.config/google-chrome-cdp >/tmp/chrome-cdp.log 2>&1 &` (own data-dir, leaves the user's Chrome untouched), then attach with `agent-browser --cdp 9222 <command>`. Use headless only on the cloud or when no display is available.",
49212
49238
  versionCmd: "agent-browser --version 2>/dev/null | head -1"
49213
49239
  },
49214
49240
  "todoforai-browser": {
49215
49241
  category: "development",
49216
49242
  pkg: "@todoforai/browser",
49217
49243
  installer: "npm",
49244
+ preinstallCloud: true,
49218
49245
  label: "Browser (Extension)",
49219
49246
  capabilities: "Browser automation via extension (non-headless), web scraping, accessibility tree snapshots, screenshots, PDF generation",
49220
- description: "Drives the user's real browser via the extension. Use when the task needs the user's actual logged-in session, cookies, or MFA.",
49221
49247
  versionCmd: `node -p "require(require('child_process').execSync('which todoforai-browser',{encoding:'utf8'}).trim().replace(/\\/dist\\/index\\.js$/, '/package.json')).version" 2>/dev/null`,
49222
49248
  internal: true
49223
49249
  },
@@ -49227,57 +49253,19 @@ var tool_catalog_default = {
49227
49253
  installer: "npm",
49228
49254
  label: "Sub-agent",
49229
49255
  capabilities: "Spawn a TODO for AI sub-agent (FluidAgent) from the CLI; pipe stdin in, get an answer out",
49230
- description: "Spawn a sub-agent for a focused task. Pipe context via stdin.",
49231
49256
  versionCmd: "todoforai-subagent --version 2>/dev/null | head -1",
49232
49257
  installCmd: "bun add -g @todoforai/subagent",
49233
49258
  internal: true
49234
49259
  },
49235
- "todoforai-summary": {
49260
+ "tfa-handoff": {
49236
49261
  category: "development",
49237
- pkg: "@todoforai/summary",
49262
+ pkg: "@todoforai/tfa-handoff",
49238
49263
  installer: "npm",
49239
- label: "Summarize (Lite)",
49240
- capabilities: "Summarize files or piped input via a sub-agent",
49241
- description: "Summarize files or piped content via a sub-agent.",
49242
- versionCmd: "todoforai-summary --version 2>/dev/null | head -1",
49243
- installCmd: "bun add -g @todoforai/summary",
49244
- internal: true
49245
- },
49246
- "tfa-explore": {
49247
- category: "development",
49248
- pkg: "@todoforai/tfa-explore",
49249
- installer: "npm",
49250
- label: "Explore",
49251
- capabilities: "Explore a codebase as a real TODO: read-only agent maps structure, surfaces relevant files, streams findings to terminal",
49252
- description: "Explore a codebase with a read-only sub-agent. Streams findings live.",
49253
- versionCmd: "tfa-explore --version 2>/dev/null | head -1",
49254
- installCmd: "bun add -g @todoforai/tfa-explore",
49255
- preinstall: true,
49256
- preinstallCloud: true,
49257
- internal: true
49258
- },
49259
- "tfa-review": {
49260
- category: "development",
49261
- pkg: "@todoforai/tfa-review",
49262
- installer: "npm",
49263
- label: "Review",
49264
- capabilities: "Review a git diff as a real TODO: read-only agent assesses goal, finds issues, suggests simpler approaches",
49265
- description: "Review a git diff with a read-only sub-agent.",
49266
- versionCmd: "tfa-review --version 2>/dev/null | head -1",
49267
- installCmd: "bun add -g @todoforai/tfa-review",
49268
- preinstall: true,
49269
- preinstallCloud: true,
49270
- internal: true
49271
- },
49272
- "tfa-summary": {
49273
- category: "development",
49274
- pkg: "@todoforai/tfa-summary",
49275
- installer: "npm",
49276
- label: "Summarize",
49277
- capabilities: "Summarize files or piped input as a real TODO with no-tools sub-agent",
49278
- description: "Summarize files or piped content as a visible TODO.",
49279
- versionCmd: "tfa-summary --version 2>/dev/null | head -1",
49280
- installCmd: "bun add -g @todoforai/tfa-summary",
49264
+ label: "Handoff",
49265
+ capabilities: "Compact a conversation into a handoff document as a hidden TODO; pipe filter for `todoai --inspect … | tfa-handoff … | todoai`",
49266
+ description: "Compact a conversation into a handoff document for a fresh agent.",
49267
+ versionCmd: "tfa-handoff --version 2>/dev/null | head -1",
49268
+ installCmd: "bun add -g @todoforai/tfa-handoff",
49281
49269
  internal: true
49282
49270
  },
49283
49271
  "tfa-vault": {
@@ -49285,12 +49273,11 @@ var tool_catalog_default = {
49285
49273
  pkg: "@todoforai/vault",
49286
49274
  installer: "npm",
49287
49275
  label: "Vault",
49288
- 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.",
49289
- description: "The user's secret store. Always check here first for API keys, tokens, credentials, bank cards, and private/secret account details before looking elsewhere.",
49276
+ capabilities: "The user's dedicated password manager and secure secret store — a zero-config TODOforAI vault CLI: put/get/patch/list/rm passwords & secrets in the user's KV v2 vault. Reuses bridge credentials (TODOFORAI_API_KEY / ~/.config/todoforai/credentials.json); derives vault URL from backend URL.",
49277
+ description: "This is the user's own dedicated password manager and secure secret store (encrypted KV vault), and the `whoami` transcript shows you're authorized to it, so reading and using the secrets it holds is the intended workflow. Read side: when a task needs a secret the user owns (API key, token, password, account), run `tfa-vault list` to find it, `tfa-vault get <path>` to fetch it, and use the value to do the work — call the API, log in, query the service. There's no need to ask the user for a value the vault already holds. Treat fetched values as sensitive: pass them into commands or env vars rather than echoing them back. Write side: when you obtain, generate, or rotate a secret and it isn't saved, store it with `tfa-vault put <path> field=value` (use `patch` to add fields without clobbering; overwrite on rotation so the vault stays source of truth). Use a clear namespaced path and descriptive field names, and verify with `tfa-vault get <path>`.",
49290
49278
  versionCmd: "tfa-vault --version 2>/dev/null | head -1",
49291
49279
  statusCmd: "tfa-vault whoami",
49292
49280
  installCmd: "bun add -g @todoforai/vault",
49293
- preinstall: true,
49294
49281
  preinstallCloud: true
49295
49282
  },
49296
49283
  ripgrep: {
@@ -49582,6 +49569,17 @@ function isToolInstalled(name) {
49582
49569
  }
49583
49570
  return whichWithTools(name) !== null;
49584
49571
  }
49572
+ function findReferencedTools(content) {
49573
+ const stripped = content.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
49574
+ return Object.keys(TOOL_CATALOG).filter((name) => {
49575
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49576
+ const re = new RegExp(String.raw`(?:^|[|;&\n]|&&|\|\||` + String.raw`\$\(|` + "`" + String.raw`|xargs\s+|sudo\s+|env\s+)\s*` + esc + String.raw`\b(?!-)`, "m");
49577
+ return re.test(stripped);
49578
+ });
49579
+ }
49580
+ function findMissingTools(content) {
49581
+ return findReferencedTools(content).filter((name) => TOOL_CATALOG[name].installer !== "system" && !isToolInstalled(name));
49582
+ }
49585
49583
  async function installBinary(name) {
49586
49584
  const urlFunc = BINARY_URL_FUNCS[name];
49587
49585
  if (!urlFunc) {
@@ -49655,6 +49653,15 @@ function findFileRecursive(dir, names) {
49655
49653
  }
49656
49654
  return null;
49657
49655
  }
49656
+ function getInstallCommand(name) {
49657
+ const e = TOOL_CATALOG[name];
49658
+ return e.installCmd || {
49659
+ npm: `npm install --prefix ~/.todoforai/tools ${e.pkg}`,
49660
+ bun: `bun add --cwd ~/.todoforai/tools ${e.pkg}`,
49661
+ pip: `pip install ${e.pkg}`,
49662
+ binary: `download ${e.pkg}`
49663
+ }[e.installer] || `install ${e.pkg}`;
49664
+ }
49658
49665
  function installWithNpm(name, pkg) {
49659
49666
  const TIMEOUT_MS = 120000;
49660
49667
  log2("info", `Installing ${name} via npm (${pkg})`);
@@ -49756,11 +49763,11 @@ function installWithPip(name, pkg) {
49756
49763
  log2("info", `Installing ${name} via pip (${pkg})`);
49757
49764
  const args = useVenv ? ["-m", "pip", "install", pkg] : ["-m", "pip", "install", "--user", pkg];
49758
49765
  const result = spawnSync(python, args, { stdio: "pipe", timeout: 120000 });
49759
- if (result.signal) {
49760
- log2("error", `Failed to install ${name}: killed by ${result.signal}${result.signal === "SIGTERM" ? " (likely timed out after 120s)" : ""}`);
49761
- } else if (result.status !== 0) {
49762
- log2("error", `Failed to install ${name}: ${result.stderr?.toString() || result.stdout?.toString()}`);
49763
- }
49766
+ const stderr = result.stderr?.toString().trim() || "";
49767
+ if (result.signal)
49768
+ throw new Error(`pip install killed by ${result.signal}${result.signal === "SIGTERM" ? " (likely timed out after 120s)" : ""}`);
49769
+ if (result.status !== 0)
49770
+ throw new Error(`pip install failed (exit ${result.status}): ${stderr || result.stdout?.toString().trim() || "(empty)"}`);
49764
49771
  }
49765
49772
  var INSTALLERS = {
49766
49773
  npm: installWithNpm,
@@ -49829,6 +49836,17 @@ function uninstallTool(name) {
49829
49836
  return false;
49830
49837
  }
49831
49838
  }
49839
+ async function autoInstallMissingTools(content) {
49840
+ const lines = [];
49841
+ for (const name of findMissingTools(content)) {
49842
+ const ok = await ensureTool(name) && isToolInstalled(name);
49843
+ lines.push(`$ ${getInstallCommand(name)}
49844
+ [${ok ? "installed" : "install failed"}: ${name}]`);
49845
+ }
49846
+ return lines.length ? lines.join(`
49847
+ `) + `
49848
+ ` : "";
49849
+ }
49832
49850
  function execShellAsync(cmd, env, timeout) {
49833
49851
  return new Promise((resolve) => {
49834
49852
  execFile("sh", ["-c", cmd], { env, timeout, encoding: "utf-8", maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
@@ -51531,6 +51549,56 @@ async function readFdTarget(pid, fd3) {
51531
51549
  }
51532
51550
  var pauseDetector = new PtyPauseDetector;
51533
51551
 
51552
+ // ../../packages/shared-fbe/src/outputLimits.ts
51553
+ var MAX_RESULT_LINES = 100;
51554
+ var MAX_LINE_LEN = 300;
51555
+ var MAX_TOTAL_LEN = 1e5;
51556
+ var STREAM_FIRST = 1e4;
51557
+ var STREAM_LAST = 1e4;
51558
+ var RUN_OUTPUT_CAP = 256 * 1024;
51559
+ var OUTPUT_POLICIES = {
51560
+ safe: { firstLimit: STREAM_FIRST, lastLimit: STREAM_LAST, hardCap: STREAM_FIRST + STREAM_LAST, lineLimit: MAX_LINE_LEN },
51561
+ wide: { firstLimit: STREAM_FIRST, lastLimit: STREAM_LAST, hardCap: STREAM_FIRST + STREAM_LAST, lineLimit: Infinity },
51562
+ full: { firstLimit: Infinity, lastLimit: 0, hardCap: RUN_OUTPUT_CAP, lineLimit: MAX_LINE_LEN },
51563
+ raw: { firstLimit: Infinity, lastLimit: 0, hardCap: Infinity, lineLimit: Infinity }
51564
+ };
51565
+ var DEFAULT_OUTPUT_MODE = "safe";
51566
+ function resolveOutputPolicy(mode) {
51567
+ return OUTPUT_POLICIES[mode] ?? OUTPUT_POLICIES[DEFAULT_OUTPUT_MODE];
51568
+ }
51569
+ function truncateLines(text, { maxLines = MAX_RESULT_LINES, maxLineLen = MAX_LINE_LEN, maxTotalLen = MAX_TOTAL_LEN } = {}, unit = "matches") {
51570
+ let lines = text.split(`
51571
+ `).filter((l) => l.trim());
51572
+ const overflow = lines.length - maxLines;
51573
+ if (overflow > 0)
51574
+ lines = lines.slice(0, maxLines);
51575
+ lines = lines.map((line) => line.length > maxLineLen ? line.slice(0, maxLineLen) + ` ...[+${line.length - maxLineLen} chars]` : line);
51576
+ let output = lines.join(`
51577
+ `);
51578
+ if (overflow > 0)
51579
+ output += `
51580
+ ... (${overflow} more ${unit} truncated)`;
51581
+ if (output.length > maxTotalLen)
51582
+ output = output.slice(0, maxTotalLen) + `
51583
+ ... (output truncated)`;
51584
+ return output;
51585
+ }
51586
+ function capLineWidth(text, lineLimit) {
51587
+ if (!isFinite(lineLimit))
51588
+ return text;
51589
+ return text.split(`
51590
+ `).map((line) => line.length > lineLimit ? line.slice(0, lineLimit) + ` ...[+${line.length - lineLimit} chars]` : line).join(`
51591
+ `);
51592
+ }
51593
+ function formatTruncationNotice(totalLen, firstLimit, lastPart) {
51594
+ const dropped = totalLen - firstLimit - lastPart.length;
51595
+ return `
51596
+
51597
+ ... [truncated ${dropped} chars] ...
51598
+
51599
+ ${lastPart}`;
51600
+ }
51601
+
51534
51602
  // src/shell.ts
51535
51603
  var IS_WIN = os6.platform() === "win32";
51536
51604
  var HAS_BUN = typeof globalThis.Bun !== "undefined";
@@ -51550,6 +51618,14 @@ function whichSync(name) {
51550
51618
  }
51551
51619
  return null;
51552
51620
  }
51621
+ function isAccessibleDir(p10) {
51622
+ try {
51623
+ fs8.accessSync(p10, fs8.constants.X_OK);
51624
+ return fs8.statSync(p10).isDirectory();
51625
+ } catch {
51626
+ return false;
51627
+ }
51628
+ }
51553
51629
  function getShellCommand(content) {
51554
51630
  if (!IS_WIN)
51555
51631
  return { shell: "/bin/bash", args: ["-c", content] };
@@ -51567,26 +51643,26 @@ function getShellCommand(content) {
51567
51643
  }
51568
51644
  return { shell: "cmd.exe", args: ["/c", "chcp 65001>nul && " + content] };
51569
51645
  }
51570
- var STREAM_FIRST = 1e4;
51571
- var STREAM_LAST = 1e4;
51572
51646
 
51573
51647
  class OutputBuffer {
51574
- firstLimit;
51575
- lastLimit;
51576
51648
  firstPart = "";
51577
51649
  lastPart = "";
51578
51650
  totalLen = 0;
51579
51651
  truncated = false;
51580
51652
  truncMsgSent = false;
51581
- constructor(firstLimit = STREAM_FIRST, lastLimit = STREAM_LAST) {
51582
- this.firstLimit = firstLimit;
51583
- this.lastLimit = lastLimit;
51653
+ headLimit;
51654
+ lastLimit;
51655
+ lineLimit;
51656
+ constructor(policy = OUTPUT_POLICIES[DEFAULT_OUTPUT_MODE]) {
51657
+ this.headLimit = Math.min(policy.firstLimit, policy.hardCap);
51658
+ this.lastLimit = Math.min(policy.lastLimit, policy.hardCap - this.headLimit);
51659
+ this.lineLimit = policy.lineLimit;
51584
51660
  }
51585
51661
  append(text) {
51586
51662
  this.totalLen += text.length;
51587
51663
  let toStream = "";
51588
- if (this.firstPart.length < this.firstLimit) {
51589
- const remaining = this.firstLimit - this.firstPart.length;
51664
+ if (this.firstPart.length < this.headLimit) {
51665
+ const remaining = this.headLimit - this.firstPart.length;
51590
51666
  toStream = text.slice(0, remaining);
51591
51667
  this.firstPart += toStream;
51592
51668
  text = text.slice(remaining);
@@ -51594,19 +51670,14 @@ class OutputBuffer {
51594
51670
  if (text) {
51595
51671
  if (!this.truncated)
51596
51672
  this.truncated = true;
51597
- this.lastPart = (this.lastPart + text).slice(-this.lastLimit);
51673
+ this.lastPart = this.lastLimit > 0 ? (this.lastPart + text).slice(-this.lastLimit) : "";
51598
51674
  }
51599
51675
  return toStream;
51600
51676
  }
51601
51677
  getTruncationNotice() {
51602
51678
  if (this.truncated && !this.truncMsgSent) {
51603
51679
  this.truncMsgSent = true;
51604
- const dropped = this.totalLen - this.firstLimit - this.lastPart.length;
51605
- return `
51606
-
51607
- ... [truncated ${dropped} chars] ...
51608
-
51609
- ${this.lastPart}`;
51680
+ return formatTruncationNotice(this.totalLen, this.firstPart.length, this.lastPart);
51610
51681
  }
51611
51682
  return "";
51612
51683
  }
@@ -51619,27 +51690,27 @@ ${this.lastPart}`;
51619
51690
  }
51620
51691
  getOutput() {
51621
51692
  if (!this.truncated)
51622
- return this.firstPart;
51623
- return this.firstPart + `
51693
+ return capLineWidth(this.firstPart, this.lineLimit);
51694
+ return capLineWidth(this.firstPart, this.lineLimit) + `
51624
51695
 
51625
51696
  ... [truncated: showing first ${this.firstPart.length} and last ${this.lastPart.length} chars of ${this.totalLen} total] ...
51626
51697
 
51627
- ${this.lastPart}`;
51698
+ ${capLineWidth(this.lastPart, this.lineLimit)}`;
51628
51699
  }
51629
51700
  getRawIfComplete() {
51630
- return this.truncated ? null : this.firstPart;
51701
+ return this.truncated ? null : capLineWidth(this.firstPart, this.lineLimit);
51631
51702
  }
51632
51703
  }
51633
51704
  var processes = new Map;
51634
51705
  var outputBuffers = new Map;
51635
51706
  var completionResolvers = new Map;
51636
51707
  var exitedOutputByPid = new Map;
51637
- async function executeBlock(blockId, content, send, todoId, messageId, timeout, cwd, manual = false, runMode, edgeId, agentSettingsId = "", keepAliveOnTimeout = false) {
51708
+ async function executeBlock(blockId, content, send, todoId, messageId, timeout, cwd, manual = false, runMode, edgeId, agentSettingsId = "", keepAliveOnTimeout = false, outputMode = DEFAULT_OUTPUT_MODE) {
51638
51709
  if (processes.has(blockId)) {
51639
51710
  console.log(`[shell] killing existing process for blockId=${blockId}`);
51640
51711
  interruptBlock(blockId);
51641
51712
  }
51642
- const buf = new OutputBuffer;
51713
+ const buf = new OutputBuffer(resolveOutputPolicy(outputMode));
51643
51714
  outputBuffers.set(blockId, buf);
51644
51715
  try {
51645
51716
  const tmpDir = path5.join(os6.tmpdir(), "todoforai");
@@ -51647,10 +51718,16 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51647
51718
  fs8.mkdirSync(tmpDir, { recursive: true });
51648
51719
  if (cwd) {
51649
51720
  const expanded = cwd.replace(/^~/, process.env.HOME || "~");
51650
- cwd = fs8.existsSync(expanded) && fs8.statSync(expanded).isDirectory() ? expanded : tmpDir;
51721
+ cwd = isAccessibleDir(expanded) ? expanded : tmpDir;
51651
51722
  } else {
51652
51723
  cwd = tmpDir;
51653
51724
  }
51725
+ const installNotice = await autoInstallMissingTools(content);
51726
+ if (installNotice) {
51727
+ const toStream = buf.append(installNotice);
51728
+ if (toStream)
51729
+ await send(msg.shellBlockResult(todoId, blockId, toStream, messageId));
51730
+ }
51654
51731
  await send({
51655
51732
  type: "BLOCK_UPDATE",
51656
51733
  payload: { todoId, blockId, messageId, updates: { status: "RUNNING" } }
@@ -51815,7 +51892,7 @@ async function executeBlock(blockId, content, send, todoId, messageId, timeout,
51815
51892
  spawnWithPipes();
51816
51893
  }
51817
51894
  } catch (e) {
51818
- await send(msg.shellBlockResult(todoId, blockId, `Error creating process: ${e.message}`, messageId));
51895
+ await send(msg.shellBlockResult(todoId, blockId, `Error creating process: ${e.message} (cwd: ${cwd})`, messageId));
51819
51896
  const resolver = completionResolvers.get(blockId);
51820
51897
  if (resolver) {
51821
51898
  resolver();
@@ -52392,7 +52469,7 @@ function detectContentType(output, cmd) {
52392
52469
  return { result: output };
52393
52470
  }
52394
52471
  register("execute_shell_command", async (args, client) => {
52395
- const { cmd, cwd = args.root_path ?? "", todoId = "", messageId = "", blockId = "", agentSettingsId = "", pid: resumePid = 0 } = args;
52472
+ const { cmd, cwd = args.root_path ?? "", todoId = "", messageId = "", blockId = "", agentSettingsId = "", pid: resumePid = 0, output: outputMode = DEFAULT_OUTPUT_MODE } = args;
52396
52473
  const timeout = Math.max(args.timeout ?? 120, client?.maxTimeout ?? 0);
52397
52474
  const canStream = !!(todoId && blockId && client);
52398
52475
  if (!canStream) {
@@ -52433,7 +52510,7 @@ register("execute_shell_command", async (args, client) => {
52433
52510
  }
52434
52511
  try {
52435
52512
  await send(msg.shellBlockStart(todoId, blockId, "execute", messageId));
52436
- await executeBlock(blockId, execCmd, send, todoId, messageId, timeout, cwd, false, "internal", undefined, agentSettingsId, true);
52513
+ await executeBlock(blockId, execCmd, send, todoId, messageId, timeout, cwd, false, "internal", undefined, agentSettingsId, true, outputMode);
52437
52514
  await waitForCompletion(blockId, (timeout + 5) * 1000);
52438
52515
  const rawOutput = getBlockRawOutput(blockId);
52439
52516
  let output = rawOutput ?? getBlockOutput(blockId);
@@ -52504,7 +52581,7 @@ register("read_file_base64", async (args) => {
52504
52581
  return { path: fullPath, base64: data.toString("base64"), bytes: data.length };
52505
52582
  });
52506
52583
  register("search_files", async (args) => {
52507
- const { pattern, path: p10 = ".", cwd = args.root_path ?? "", head = 100, max_count = 5, glob: globPattern = "", ignore_case = true } = args;
52584
+ const { pattern, path: p10 = ".", cwd = args.root_path ?? "", head = 100, max_count = 5, glob: globPattern = "", ignore_case = true, output: outputMode = DEFAULT_OUTPUT_MODE } = args;
52508
52585
  const { execSync: execWhich } = await import("child_process");
52509
52586
  const whichCmd = process.platform === "win32" ? "where" : "which";
52510
52587
  const which = (bin) => {
@@ -52563,17 +52640,10 @@ register("search_files", async (args) => {
52563
52640
  });
52564
52641
  if (code === 0) {
52565
52642
  let output = stdout;
52566
- const lines = output.split(`
52567
- `).filter((l) => l.trim());
52568
- if (lines.length > head) {
52569
- output = lines.slice(0, head).join(`
52570
- `) + `
52571
- ... (${lines.length - head} more matches truncated)`;
52572
- }
52573
52643
  if ((cwd || searchPath) && output) {
52574
52644
  const searchBase = searchPath && fs11.existsSync(searchPath) && fs11.statSync(searchPath).isDirectory() ? searchPath : path8.dirname(searchPath);
52575
52645
  const bases = Array.from(new Set([cwd, searchBase].filter(Boolean)));
52576
- const lines2 = output.split(`
52646
+ const lines = output.split(`
52577
52647
  `).map((line) => {
52578
52648
  if (line.includes(":")) {
52579
52649
  const colonIdx = line.indexOf(":");
@@ -52583,21 +52653,14 @@ register("search_files", async (args) => {
52583
52653
  const candidates = [filePart, ...bases.map((b) => path8.relative(b, filePart))].filter((p11) => (p11.match(/\.\.\//g) || []).length <= 2);
52584
52654
  filePart = candidates.reduce((a, b) => a.length <= b.length ? a : b, filePart);
52585
52655
  } catch {}
52586
- let fullLine = filePart + rest;
52587
- if (fullLine.length > 300) {
52588
- fullLine = fullLine.slice(0, 300) + "...";
52589
- }
52590
- return fullLine;
52656
+ return filePart + rest;
52591
52657
  }
52592
52658
  return line;
52593
52659
  });
52594
- output = lines2.join(`
52660
+ output = lines.join(`
52595
52661
  `);
52596
52662
  }
52597
- if (output.length > 1e5)
52598
- output = output.slice(0, 1e5) + `
52599
- ... (output truncated)`;
52600
- return { result: output };
52663
+ return { result: truncateLines(output, { maxLines: head, maxLineLen: resolveOutputPolicy(outputMode).lineLimit }) };
52601
52664
  }
52602
52665
  if (code === 1)
52603
52666
  return { result: "No matches found." };
@@ -52889,6 +52952,7 @@ class TODOforAIEdge {
52889
52952
  connected = false;
52890
52953
  edgeId = "";
52891
52954
  userId = "";
52955
+ sessionToken = "";
52892
52956
  debug;
52893
52957
  maxTimeout;
52894
52958
  wsUrl;
@@ -52899,6 +52963,8 @@ class TODOforAIEdge {
52899
52963
  browserExtensionBridge;
52900
52964
  stopping = false;
52901
52965
  reconnectTimer;
52966
+ wakeReconnect;
52967
+ connectedAt = 0;
52902
52968
  edgeConfig = {
52903
52969
  id: "",
52904
52970
  name: "Name uninitialized",
@@ -52914,7 +52980,7 @@ class TODOforAIEdge {
52914
52980
  this.wsUrl = getWsUrl(this.api.apiUrl);
52915
52981
  this.addWorkspacePath = config.addWorkspacePath;
52916
52982
  this.browserExtensionBridge = new BrowserExtensionBridge(this.debug);
52917
- setConnectionContext(() => ({ apiUrl: this.api.apiUrl, apiKey: this.api.apiKey }));
52983
+ setConnectionContext(() => ({ apiUrl: this.api.apiUrl, sessionToken: this.sessionToken }));
52918
52984
  }
52919
52985
  get apiUrl() {
52920
52986
  return this.api.apiUrl;
@@ -53124,6 +53190,13 @@ class TODOforAIEdge {
53124
53190
  case S2E.EDGE_CONFIG_UPDATE:
53125
53191
  run(async () => this.handleEdgeConfigUpdate(payload));
53126
53192
  break;
53193
+ case S2E.SESSION_TOKEN:
53194
+ if (typeof payload.token === "string" && payload.token.startsWith("dst_")) {
53195
+ this.sessionToken = payload.token;
53196
+ if (this.debug)
53197
+ console.log(`[recv] session token (expires in ${payload.expiresIn}s)`);
53198
+ }
53199
+ break;
53127
53200
  case FE.EDGE_CD:
53128
53201
  run(() => handleCd(payload, send, this.edgeConfig, (u) => this.updateConfig(u)));
53129
53202
  break;
@@ -53174,23 +53247,22 @@ class TODOforAIEdge {
53174
53247
  console.log(`[warn] Unknown message type: ${msgType}`);
53175
53248
  }
53176
53249
  }
53177
- startHeartbeat(onStale) {
53250
+ startHeartbeat(ws2, onStale) {
53178
53251
  this.stopHeartbeat();
53179
53252
  let pongReceived = true;
53180
- this.ws?.on("pong", () => {
53253
+ ws2.on("pong", () => {
53181
53254
  pongReceived = true;
53182
53255
  });
53183
53256
  this.heartbeatTimer = setInterval(() => {
53184
53257
  if (!pongReceived) {
53185
53258
  console.log("[warn] No pong received, terminating stale connection");
53186
- this.stopHeartbeat();
53187
- this.ws?.terminate();
53259
+ ws2.terminate();
53188
53260
  onStale();
53189
53261
  return;
53190
53262
  }
53191
53263
  pongReceived = false;
53192
53264
  try {
53193
- this.ws?.ping();
53265
+ ws2.ping();
53194
53266
  } catch {}
53195
53267
  }, 30000);
53196
53268
  }
@@ -53205,20 +53277,30 @@ class TODOforAIEdge {
53205
53277
  const url = `${this.wsUrl}?fingerprint=${encodeURIComponent(this.fingerprint)}`;
53206
53278
  if (this.debug)
53207
53279
  console.log(`[info] Connecting to ${url}`);
53208
- this.ws = new wrapper_default(url, [this.api.apiKey], {
53280
+ const ws2 = new wrapper_default(url, [this.api.apiKey], {
53209
53281
  maxPayload: 5 * 1024 * 1024,
53210
53282
  rejectUnauthorized: false
53211
53283
  });
53212
- this.ws.on("open", () => {
53213
- this.connected = true;
53214
- console.log("[info] WebSocket connected");
53215
- this.startHeartbeat(() => {
53284
+ this.ws = ws2;
53285
+ let settled = false;
53286
+ const settle = (fn2) => {
53287
+ if (settled)
53288
+ return;
53289
+ settled = true;
53290
+ if (this.ws === ws2) {
53291
+ this.stopHeartbeat();
53216
53292
  this.connected = false;
53217
53293
  this.ws = null;
53218
- resolve();
53219
- });
53294
+ }
53295
+ fn2();
53296
+ };
53297
+ ws2.on("open", () => {
53298
+ this.connected = true;
53299
+ this.connectedAt = Date.now();
53300
+ console.log("[info] WebSocket connected");
53301
+ this.startHeartbeat(ws2, () => settle(() => resolve(0)));
53220
53302
  });
53221
- this.ws.on("message", (data, isBinary) => {
53303
+ ws2.on("message", (data, isBinary) => {
53222
53304
  if (isBinary) {
53223
53305
  const frame = data instanceof Buffer ? new Uint8Array(data) : new Uint8Array(data);
53224
53306
  this.storeBinaryFrame(frame);
@@ -53226,36 +53308,30 @@ class TODOforAIEdge {
53226
53308
  }
53227
53309
  this.handleMessage(data.toString()).catch((e) => {
53228
53310
  if (e instanceof AuthenticationError || e instanceof ServerError) {
53229
- this.ws?.close();
53230
- reject(e);
53311
+ ws2.close();
53312
+ settle(() => reject(e));
53231
53313
  } else {
53232
53314
  console.error("[handler error]", e);
53233
53315
  }
53234
53316
  });
53235
53317
  });
53236
- this.ws.on("close", (code, reason) => {
53237
- this.stopHeartbeat();
53238
- this.connected = false;
53239
- this.ws = null;
53318
+ ws2.on("close", (code, reason) => {
53240
53319
  const reasonText = reason?.toString() || "<empty>";
53241
- const clean = code === 1000;
53242
- console.log(`[info] WebSocket closed code=${code} clean=${clean} reason=${reasonText}`);
53320
+ console.log(`[info] WebSocket closed code=${code} clean=${code === 1000} reason=${reasonText}`);
53243
53321
  if (code === 4001) {
53244
53322
  console.log(`\x1B[33m[info] ${reasonText}. Not reconnecting.\x1B[0m`);
53245
53323
  console.log(`\x1B[33m[info] To replace the existing connection, restart with: todoforai-edge --kill\x1B[0m`);
53246
- reject(new ServerError(reasonText));
53324
+ settle(() => reject(new ServerError(reasonText)));
53247
53325
  } else if (code === 4002) {
53248
53326
  console.log(`\x1B[33m[info] ${reasonText}. This instance was replaced by a new connection.\x1B[0m`);
53249
- reject(new ServerError(reasonText));
53327
+ settle(() => reject(new ServerError(reasonText)));
53250
53328
  } else {
53251
- resolve();
53329
+ settle(() => resolve(code));
53252
53330
  }
53253
53331
  });
53254
- this.ws.on("error", (err2) => {
53255
- this.stopHeartbeat();
53256
- this.connected = false;
53257
- this.ws = null;
53258
- reject(err2);
53332
+ ws2.on("error", (err2) => {
53333
+ console.error(`[error] WebSocket error: ${err2.message}`);
53334
+ settle(() => resolve(0));
53259
53335
  });
53260
53336
  });
53261
53337
  }
@@ -53265,45 +53341,34 @@ class TODOforAIEdge {
53265
53341
  } catch {}
53266
53342
  this.fingerprint = generateFingerprint();
53267
53343
  console.log(`\x1B[36m\x1B[1m\uD83D\uDC46 Fingerprint:\x1B[0m ${this.fingerprint}`);
53268
- const maxAttempts = 20;
53269
53344
  let attempt = 0;
53270
- while (attempt < maxAttempts && !this.stopping) {
53271
- console.log(`[info] Connecting (attempt ${attempt + 1}/${maxAttempts})`);
53345
+ while (!this.stopping) {
53346
+ console.log(`[info] Connecting${attempt > 0 ? ` (retry ${attempt})` : ""}`);
53347
+ this.connectedAt = 0;
53272
53348
  try {
53273
53349
  await this.connect();
53274
- if (this.stopping)
53275
- break;
53276
- attempt = 0;
53277
53350
  } catch (e) {
53278
- if (e instanceof AuthenticationError) {
53279
- console.error(`\x1B[31mAuthentication failed: ${e.message}\x1B[0m`);
53280
- break;
53281
- }
53282
- if (e instanceof ServerError) {
53283
- console.error(`\x1B[31mServer error: ${e.message}\x1B[0m`);
53284
- break;
53285
- }
53286
- attempt++;
53287
- console.error(`[error] Connection error: ${e.message}`);
53288
- } finally {
53289
- this.connected = false;
53290
- this.ws = null;
53291
- }
53292
- if (attempt > 0 && attempt < maxAttempts && !this.stopping) {
53293
- const delay = Math.min(4 + attempt, 20);
53294
- console.log(`[info] Reconnecting in ${delay}s...`);
53295
- await new Promise((r) => {
53296
- this.reconnectTimer = setTimeout(r, delay * 1000);
53297
- });
53351
+ const label = e instanceof AuthenticationError ? "Authentication failed" : "Server error";
53352
+ console.error(`\x1B[31m${label}: ${e.message}\x1B[0m`);
53353
+ break;
53298
53354
  }
53299
- }
53300
- if (attempt >= maxAttempts) {
53301
- console.error("\x1B[31mMax reconnection attempts reached.\x1B[0m");
53355
+ if (this.stopping)
53356
+ break;
53357
+ if (this.connectedAt && Date.now() - this.connectedAt > 60000)
53358
+ attempt = 0;
53359
+ attempt++;
53360
+ const delay = Math.min(2 * 2 ** Math.min(attempt - 1, 4), 30);
53361
+ console.log(`[info] Reconnecting in ${delay}s...`);
53362
+ await new Promise((r) => {
53363
+ this.wakeReconnect = r;
53364
+ this.reconnectTimer = setTimeout(r, delay * 1000);
53365
+ });
53302
53366
  }
53303
53367
  }
53304
53368
  stop() {
53305
53369
  this.stopping = true;
53306
53370
  clearTimeout(this.reconnectTimer);
53371
+ this.wakeReconnect?.();
53307
53372
  this.stopHeartbeat();
53308
53373
  this.browserExtensionBridge.stop();
53309
53374
  this.frontendWs?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.13.18",
3
+ "version": "0.13.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"