@todoforai/edge 0.13.20 → 0.13.22

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 +139 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -48153,6 +48153,27 @@ class ApiClient {
48153
48153
  throw new Error(`API ${method} ${endpoint} failed: ${res.status} ${await res.text()}`);
48154
48154
  return res.json();
48155
48155
  }
48156
+ async trpcQuery(path2, input) {
48157
+ const base = this.apiUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
48158
+ const params = new URLSearchParams({
48159
+ batch: "1",
48160
+ input: JSON.stringify({ 0: input })
48161
+ });
48162
+ const res = await fetch(`${base}/trpc/api/${path2}?${params}`, {
48163
+ method: "GET",
48164
+ headers: this.headers,
48165
+ signal: AbortSignal.timeout(30000)
48166
+ });
48167
+ const text = await res.text();
48168
+ if (!res.ok)
48169
+ throw new Error(`API tRPC ${path2} failed: ${res.status} ${text}`);
48170
+ const payload = JSON.parse(text);
48171
+ const item = Array.isArray(payload) ? payload[0] : payload;
48172
+ if (item?.error)
48173
+ throw new Error(`API tRPC ${path2} failed: ${item.error.message || JSON.stringify(item.error)}`);
48174
+ const data = item?.result?.data;
48175
+ return data?.json !== undefined ? data.json : data;
48176
+ }
48156
48177
  async validateApiKey() {
48157
48178
  if (!this.apiKey)
48158
48179
  return { valid: false, error: "No API key provided" };
@@ -48191,9 +48212,10 @@ class ApiClient {
48191
48212
  createTodo(projectId, content, agentSettings) {
48192
48213
  return this.request("POST", `/api/v1/projects/${projectId}/todos`, { content, agentSettings });
48193
48214
  }
48194
- listTodos(projectId) {
48195
- const endpoint = projectId ? `/api/v1/projects/${projectId}/todos` : "/api/v1/todos";
48196
- return this.request("GET", endpoint);
48215
+ listTodos(projectId, opts) {
48216
+ if (!projectId)
48217
+ return this.request("GET", "/api/v1/todos");
48218
+ return this.trpcQuery("todo.list", { projectId, ...opts });
48197
48219
  }
48198
48220
  getTodo(todoId) {
48199
48221
  return this.request("GET", `/api/v1/todos/${todoId}`);
@@ -48788,6 +48810,20 @@ var tool_catalog_default = {
48788
48810
  description: "Authenticated raw X API access via `xurl <method> <path>`; login `xurl auth oauth2`.",
48789
48811
  versionCmd: "xurl --version 2>/dev/null | head -1"
48790
48812
  },
48813
+ "linkedin-api": {
48814
+ category: "social",
48815
+ pkg: "@todoforai/linkedin-api",
48816
+ installer: "npm",
48817
+ label: "LinkedIn",
48818
+ statusCmd: `linkedin-api whoami 2>/dev/null | grep -oP '"publicId":\\s*"\\K[^"]+' | head -1`,
48819
+ loginCmd: "linkedin-api auth",
48820
+ credentialPaths: [
48821
+ "~/.config/linkedin-api/credentials.json"
48822
+ ],
48823
+ capabilities: "Read profiles, list & read conversations, send DMs, send connection requests — raw LinkedIn Voyager API via cookie auth",
48824
+ description: "Authenticated LinkedIn Voyager API CLI. Cookie auth: `linkedin-api auth <li_at> <jsessionid>` (from browser DevTools).",
48825
+ versionCmd: "linkedin-api --version 2>/dev/null | head -1"
48826
+ },
48791
48827
  "tiktok-uploader": {
48792
48828
  category: "social",
48793
48829
  pkg: "tiktok-uploader",
@@ -48821,7 +48857,7 @@ var tool_catalog_default = {
48821
48857
  capabilities: "Send messages, send images & files, send locations & polls, group management, QR code login",
48822
48858
  versionCmd: "mudslide --version 2>/dev/null | head -1"
48823
48859
  },
48824
- slack: {
48860
+ "slack-cli": {
48825
48861
  category: "development",
48826
48862
  pkg: "slack-cli",
48827
48863
  installer: "binary",
@@ -49118,10 +49154,11 @@ var tool_catalog_default = {
49118
49154
  pkg: "@todoforai/cli",
49119
49155
  installer: "bun",
49120
49156
  label: "TODOforAI",
49157
+ binName: "todoforai-cli",
49121
49158
  capabilities: "Create/list/inspect/update TODOs, run templates & workflows, platform API access",
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.',
49159
+ description: "Reach the user's OWN TODOforAI tasks — NOT code `// TODO` comments. Use whenever the user asks about their tasks/todos/account on the platform: `todoforai-cli list` (`--status open`) to browse, `todoforai-cli \"prompt\"` to create, `todoforai-cli --inspect <id>[:<msg-id>]` to read a TODO's full chat/data (read-only), `status/addmessage/delete` to manage, `--template` for registry workflows. Never claim you lack access to platform TODOs — they're reachable here.",
49123
49160
  installCmd: "bun add -g @todoforai/cli",
49124
- versionCmd: "todoai --version 2>/dev/null | head -1",
49161
+ versionCmd: "todoforai-cli --version 2>/dev/null | head -1",
49125
49162
  internal: true
49126
49163
  },
49127
49164
  newman: {
@@ -49218,6 +49255,20 @@ var tool_catalog_default = {
49218
49255
  },
49219
49256
  versionCmd: "rclone version 2>/dev/null | head -1"
49220
49257
  },
49258
+ rdt: {
49259
+ category: "social",
49260
+ pkg: "rdt-cli",
49261
+ installer: "pip",
49262
+ label: "Reddit",
49263
+ statusCmd: `rdt status 2>&1 | grep -oP 'username":\\s*"\\K[^"]+' | head -1`,
49264
+ loginCmd: "rdt login",
49265
+ credentialPaths: [
49266
+ "~/.config/rdt-cli/credential.json"
49267
+ ],
49268
+ capabilities: "Search & browse subreddits, read posts & comments, post comments, upvote & save, view user profiles & comments, export results — Reddit in your terminal.",
49269
+ description: 'Reddit CLI. Read/research: `rdt search "<query>" -r <sub> --json -c`, `rdt sub <name>`, `rdt read <postId>`, `rdt user-comments <user>`. Write (needs login): `rdt comment <postId> "<text>"`, `rdt upvote`, `rdt save`. `rdt status` for auth. Cookie auth via `rdt login` (extracts browser cookies).',
49270
+ versionCmd: "rdt --version 2>/dev/null | head -1"
49271
+ },
49221
49272
  pymupdf: {
49222
49273
  category: "utility",
49223
49274
  pkg: "pymupdf",
@@ -49237,6 +49288,41 @@ var tool_catalog_default = {
49237
49288
  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.",
49238
49289
  versionCmd: "agent-browser --version 2>/dev/null | head -1"
49239
49290
  },
49291
+ "browser-manager-cli": {
49292
+ category: "development",
49293
+ pkg: "browser-manager-cli",
49294
+ installer: "binary",
49295
+ binName: "browser-manager-cli",
49296
+ preinstallCloud: true,
49297
+ label: "Browser Manager",
49298
+ statusCmd: "browser-manager-cli whoami",
49299
+ loginCmd: "browser-manager-cli login",
49300
+ capabilities: "On-demand cloud Chromium sessions exposed as CDP endpoints for agent-browser; create/list/get/delete sessions, hibernate/restore, health",
49301
+ description: "Spawns on-demand cloud Chromium sessions and exposes each as a CDP WebSocket for agent-browser to drive. Reuses the bridge login (zero-config, same creds as the daemon). Flow: `browser-manager-cli create` → prints a session with a cdpUrl; `browser-manager-cli connect <id> --exec` connects agent-browser to that session (runs `agent-browser connect '<cdp_url>'`), then drive it with normal agent-browser commands (open/click/snapshot/...). `browser-manager-cli list` shows your sessions, `whoami` the logged-in user, `delete <id>` / `delete-all` to clean up, `hibernate`/`restore` to pause/resume. Use this on the cloud VM instead of launching a local browser.",
49302
+ versionCmd: "browser-manager-cli version 2>/dev/null | head -1",
49303
+ binary: {
49304
+ "linux-x86_64": {
49305
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.0/browser-manager-cli-linux-x86_64",
49306
+ archive: "raw"
49307
+ },
49308
+ "linux-aarch64": {
49309
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.0/browser-manager-cli-linux-aarch64",
49310
+ archive: "raw"
49311
+ },
49312
+ "darwin-x86_64": {
49313
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.0/browser-manager-cli-darwin-x86_64",
49314
+ archive: "raw"
49315
+ },
49316
+ "darwin-aarch64": {
49317
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.0/browser-manager-cli-darwin-aarch64",
49318
+ archive: "raw"
49319
+ },
49320
+ "windows-x86_64": {
49321
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.0/browser-manager-cli-windows-x86_64.exe",
49322
+ archive: "raw"
49323
+ }
49324
+ }
49325
+ },
49240
49326
  "todoforai-browser": {
49241
49327
  category: "development",
49242
49328
  pkg: "@todoforai/browser",
@@ -49262,7 +49348,7 @@ var tool_catalog_default = {
49262
49348
  pkg: "@todoforai/tfa-handoff",
49263
49349
  installer: "npm",
49264
49350
  label: "Handoff",
49265
- capabilities: "Compact a conversation into a handoff document as a hidden TODO; pipe filter for `todoai --inspect … | tfa-handoff … | todoai`",
49351
+ capabilities: "Compact a conversation into a handoff document as a hidden TODO; pipe filter for `todoforai-cli --inspect … | tfa-handoff … | todoforai-cli`",
49266
49352
  description: "Compact a conversation into a handoff document for a fresh agent.",
49267
49353
  versionCmd: "tfa-handoff --version 2>/dev/null | head -1",
49268
49354
  installCmd: "bun add -g @todoforai/tfa-handoff",
@@ -49274,7 +49360,7 @@ var tool_catalog_default = {
49274
49360
  installer: "npm",
49275
49361
  label: "Vault",
49276
49362
  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>`.",
49363
+ 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 — be EXTREMELY proactive about never letting the user lose a secret: the moment ANY new secret or private detail appears or is created in the conversation — API key, token (stream/access/refresh/OAuth), password, login/account credential, connection string, private key, SSH key, recovery/backup code, card number, bank/IBAN, seed phrase, etc. — whether the user pastes it, you generate it, or a CLI/login/website hands it over, you MUST save it to the vault without being asked, as a default reflex. Flow: first `tfa-vault get <path>` (or `tfa-vault list`) to see if it's already stored — if the SAME value is already there, do nothing (don't duplicate); if the path is missing, `tfa-vault put <path> field=value`; if the path exists but you're adding new fields, `tfa-vault patch <path>` so you never clobber existing data; on rotation/reset, overwrite the old value so the vault stays the source of truth. Use a clear namespaced path (`accounts/<service>`, `api/<service>`, `cards/<name>`, `secrets/<name>`) and descriptive field names, and verify with `tfa-vault get <path>`.",
49278
49364
  versionCmd: "tfa-vault --version 2>/dev/null | head -1",
49279
49365
  statusCmd: "tfa-vault whoami",
49280
49366
  installCmd: "bun add -g @todoforai/vault",
@@ -49558,6 +49644,9 @@ function whichWithTools(name) {
49558
49644
  }
49559
49645
  return null;
49560
49646
  }
49647
+ function binFileName(name) {
49648
+ return TOOL_CATALOG[name]?.binName ?? name;
49649
+ }
49561
49650
  function isToolInstalled(name) {
49562
49651
  const entry = TOOL_CATALOG[name];
49563
49652
  if (!entry)
@@ -49567,14 +49656,17 @@ function isToolInstalled(name) {
49567
49656
  const r = spawnSync("sh", ["-c", checkCmd], { stdio: "pipe", timeout: 5000 });
49568
49657
  return r.status === 0;
49569
49658
  }
49570
- return whichWithTools(name) !== null;
49659
+ return whichWithTools(binFileName(name)) !== null;
49571
49660
  }
49572
49661
  function findReferencedTools(content) {
49573
49662
  const stripped = content.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
49574
49663
  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);
49664
+ const tokens = [name, TOOL_CATALOG[name].binName].filter((t) => !!t);
49665
+ return tokens.some((tok) => {
49666
+ const esc = tok.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49667
+ const re = new RegExp(String.raw`(?:^|[|;&\n]|&&|\|\||` + String.raw`\$\(|` + "`" + String.raw`|xargs\s+|sudo\s+|env\s+)\s*` + esc + String.raw`\b(?!-)`, "m");
49668
+ return re.test(stripped);
49669
+ });
49578
49670
  });
49579
49671
  }
49580
49672
  function findMissingTools(content) {
@@ -49589,7 +49681,8 @@ async function installBinary(name) {
49589
49681
  const dir = binDir();
49590
49682
  fs3.mkdirSync(dir, { recursive: true });
49591
49683
  const [url, isArchive] = await urlFunc();
49592
- const destName = os3.platform() === "win32" ? `${name}.exe` : name;
49684
+ const fileName = binFileName(name);
49685
+ const destName = os3.platform() === "win32" ? `${fileName}.exe` : fileName;
49593
49686
  const dest = path3.join(dir, destName);
49594
49687
  const tmpPath = dest + ".tmp";
49595
49688
  log2("info", `Downloading ${name} from ${url}`);
@@ -49599,7 +49692,7 @@ async function installBinary(name) {
49599
49692
  const data = Buffer.from(await res.arrayBuffer());
49600
49693
  fs3.writeFileSync(tmpPath, data);
49601
49694
  if (isArchive) {
49602
- const expectedNames = new Set([name, `${name}.exe`]);
49695
+ const expectedNames = new Set([name, `${name}.exe`, fileName, `${fileName}.exe`]);
49603
49696
  if (url.endsWith(".tar.gz") || url.endsWith(".tgz")) {
49604
49697
  await extractTarBinary(tmpPath, dest, expectedNames);
49605
49698
  } else if (url.endsWith(".zip")) {
@@ -51590,6 +51683,13 @@ function capLineWidth(text, lineLimit) {
51590
51683
  `).map((line) => line.length > lineLimit ? line.slice(0, lineLimit) + ` ...[+${line.length - lineLimit} chars]` : line).join(`
51591
51684
  `);
51592
51685
  }
51686
+ function collapseCarriageReturns(text) {
51687
+ if (!text.includes("\r"))
51688
+ return text;
51689
+ return text.split(`
51690
+ `).map((line) => line.includes("\r") ? line.split("\r").at(-1) ?? "" : line).join(`
51691
+ `);
51692
+ }
51593
51693
  function formatTruncationNotice(totalLen, firstLimit, lastPart) {
51594
51694
  const dropped = totalLen - firstLimit - lastPart.length;
51595
51695
  return `
@@ -51690,15 +51790,18 @@ class OutputBuffer {
51690
51790
  }
51691
51791
  getOutput() {
51692
51792
  if (!this.truncated)
51693
- return capLineWidth(this.firstPart, this.lineLimit);
51694
- return capLineWidth(this.firstPart, this.lineLimit) + `
51793
+ return this.format(this.firstPart);
51794
+ return this.format(this.firstPart) + `
51695
51795
 
51696
51796
  ... [truncated: showing first ${this.firstPart.length} and last ${this.lastPart.length} chars of ${this.totalLen} total] ...
51697
51797
 
51698
- ${capLineWidth(this.lastPart, this.lineLimit)}`;
51798
+ ${this.format(this.lastPart)}`;
51699
51799
  }
51700
51800
  getRawIfComplete() {
51701
- return this.truncated ? null : capLineWidth(this.firstPart, this.lineLimit);
51801
+ return this.truncated ? null : this.format(this.firstPart);
51802
+ }
51803
+ format(part) {
51804
+ return capLineWidth(collapseCarriageReturns(part), this.lineLimit);
51702
51805
  }
51703
51806
  }
51704
51807
  var processes = new Map;
@@ -53249,14 +53352,30 @@ class TODOforAIEdge {
53249
53352
  }
53250
53353
  startHeartbeat(ws2, onStale) {
53251
53354
  this.stopHeartbeat();
53355
+ const intervalMs = 30000;
53356
+ const resumeGapMs = 60000;
53357
+ let lastTick = Date.now();
53252
53358
  let pongReceived = true;
53253
53359
  ws2.on("pong", () => {
53254
53360
  pongReceived = true;
53255
53361
  });
53256
53362
  this.heartbeatTimer = setInterval(() => {
53363
+ const now = Date.now();
53364
+ const elapsed = now - lastTick;
53365
+ lastTick = now;
53366
+ if (elapsed > resumeGapMs) {
53367
+ console.log(`[warn] Heartbeat delayed ${Math.round(elapsed / 1000)}s (sleep/resume likely), reconnecting`);
53368
+ try {
53369
+ ws2.terminate();
53370
+ } catch {}
53371
+ onStale();
53372
+ return;
53373
+ }
53257
53374
  if (!pongReceived) {
53258
53375
  console.log("[warn] No pong received, terminating stale connection");
53259
- ws2.terminate();
53376
+ try {
53377
+ ws2.terminate();
53378
+ } catch {}
53260
53379
  onStale();
53261
53380
  return;
53262
53381
  }
@@ -53264,7 +53383,7 @@ class TODOforAIEdge {
53264
53383
  try {
53265
53384
  ws2.ping();
53266
53385
  } catch {}
53267
- }, 30000);
53386
+ }, intervalMs);
53268
53387
  }
53269
53388
  stopHeartbeat() {
53270
53389
  if (this.heartbeatTimer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/edge",
3
- "version": "0.13.20",
3
+ "version": "0.13.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-edge": "dist/index.js"