@todoforai/cli 0.1.54 → 0.1.56

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.
@@ -42926,6 +42926,19 @@ class ApiClient {
42926
42926
  upsertProjectGroup(projectId, slug, fields) {
42927
42927
  return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
42928
42928
  }
42929
+ listFeed(opts = {}) {
42930
+ const qs = new URLSearchParams({ all: "true" });
42931
+ for (const [k, v] of Object.entries(opts))
42932
+ if (v !== undefined)
42933
+ qs.set(k, String(v));
42934
+ return this.request("GET", `/api/v1/feed?${qs}`);
42935
+ }
42936
+ markFeedSeen(opts = {}) {
42937
+ return this.request("POST", "/api/v1/feed/seen", { all: true, ...opts });
42938
+ }
42939
+ sendFeedMessage(msg) {
42940
+ return this.request("POST", "/api/v1/feed/message", msg);
42941
+ }
42929
42942
  listProjectSurfaces(projectId) {
42930
42943
  return this.request("GET", `/api/v1/projects/${projectId}/surfaces`);
42931
42944
  }
@@ -43207,6 +43220,7 @@ class FrontendWebSocket {
43207
43220
  apiUrl;
43208
43221
  apiKey;
43209
43222
  ws = null;
43223
+ connecting = null;
43210
43224
  tabId = randomUUID();
43211
43225
  connected = false;
43212
43226
  callbacks = new Map;
@@ -43230,28 +43244,61 @@ class FrontendWebSocket {
43230
43244
  wsUrl = `wss://${url}`;
43231
43245
  return `${wsUrl}/ws/v1/frontend?tabId=${this.tabId}`;
43232
43246
  }
43233
- async connect() {
43247
+ connect() {
43234
43248
  if (this.connected && this.ws)
43235
- return true;
43249
+ return Promise.resolve(true);
43250
+ this.connecting ??= this.open().finally(() => {
43251
+ this.connecting = null;
43252
+ });
43253
+ return this.connecting;
43254
+ }
43255
+ open() {
43236
43256
  return new Promise((resolve) => {
43237
43257
  const wsUrl = this.getWsUrl();
43238
- this.ws = new WebSocket(wsUrl, [this.apiKey], { maxPayload: 5 * 1024 * 1024 });
43239
- this.ws.on("open", () => {
43258
+ const sock = new WebSocket(wsUrl, [this.apiKey], { maxPayload: 5 * 1024 * 1024, handshakeTimeout: 1e4 });
43259
+ this.ws = sock;
43260
+ const current = () => this.ws === sock;
43261
+ let alive = true;
43262
+ let heartbeat;
43263
+ sock.on("pong", () => {
43264
+ alive = true;
43265
+ });
43266
+ sock.on("open", () => {
43267
+ if (!current())
43268
+ return;
43240
43269
  this.connected = true;
43270
+ heartbeat = setInterval(() => {
43271
+ if (!alive) {
43272
+ sock.terminate();
43273
+ return;
43274
+ }
43275
+ alive = false;
43276
+ try {
43277
+ sock.ping();
43278
+ } catch {}
43279
+ }, 15000);
43280
+ heartbeat.unref?.();
43241
43281
  resolve(true);
43242
43282
  });
43243
- this.ws.on("message", (data) => {
43283
+ sock.on("message", (data) => {
43284
+ if (!current())
43285
+ return;
43244
43286
  try {
43245
43287
  this.handleMessage(JSON.parse(data.toString()));
43246
43288
  } catch {}
43247
43289
  });
43248
- this.ws.on("close", () => {
43290
+ sock.on("close", () => {
43291
+ clearInterval(heartbeat);
43292
+ if (!current())
43293
+ return;
43249
43294
  this.connected = false;
43250
43295
  this.settleAll();
43296
+ resolve(false);
43251
43297
  });
43252
- this.ws.on("error", (err) => {
43298
+ sock.on("error", (err) => {
43253
43299
  log("error", "WS error:", err.message);
43254
- this.connected = false;
43300
+ if (current())
43301
+ this.connected = false;
43255
43302
  resolve(false);
43256
43303
  });
43257
43304
  });
@@ -43320,7 +43367,7 @@ class FrontendWebSocket {
43320
43367
  }
43321
43368
  }
43322
43369
  }
43323
- async subscribe(todoId, callback) {
43370
+ async subscribe(todoId, callback, opts = {}) {
43324
43371
  if (!this.connected && !await this.connect())
43325
43372
  return false;
43326
43373
  if (callback)
@@ -43328,8 +43375,16 @@ class FrontendWebSocket {
43328
43375
  const res = await fetch(`${this.apiUrl}/api/v1/todos/${todoId}/subscribe`, {
43329
43376
  method: "POST",
43330
43377
  headers: { "x-api-key": this.apiKey, "x-tab-id": this.tabId, "Content-Type": "application/json" },
43331
- body: JSON.stringify({ todoId })
43378
+ body: JSON.stringify({ todoId }),
43379
+ signal: AbortSignal.timeout(15000)
43332
43380
  });
43381
+ if (res.ok && opts.replayStatus) {
43382
+ this.pendingBlocks.delete(todoId);
43383
+ this.lastReadyPayload.delete(todoId);
43384
+ const status = (await res.json().catch(() => null))?.status;
43385
+ if (status)
43386
+ this.handleMessage({ type: "todo:status", payload: { todoId, status } });
43387
+ }
43333
43388
  return res.ok;
43334
43389
  }
43335
43390
  setCallback(todoId, callback) {
@@ -43339,17 +43394,16 @@ class FrontendWebSocket {
43339
43394
  this.callbacks.delete(todoId);
43340
43395
  }
43341
43396
  async sendInterrupt(projectId, todoId) {
43342
- if (!this.connected || !this.ws)
43343
- return false;
43344
- try {
43345
- this.ws.send(JSON.stringify({
43346
- type: "todo:interrupt_signal",
43347
- payload: { projectId, todoId }
43348
- }));
43349
- return true;
43350
- } catch {
43397
+ const ws = this.ws;
43398
+ if (!this.connected || !ws)
43351
43399
  return false;
43352
- }
43400
+ return new Promise((resolve) => {
43401
+ try {
43402
+ ws.send(JSON.stringify({ type: "todo:interrupt_signal", payload: { projectId, todoId } }), (err) => resolve(!err));
43403
+ } catch {
43404
+ resolve(false);
43405
+ }
43406
+ });
43353
43407
  }
43354
43408
  async sendQueueCancel(todoId, messageId) {
43355
43409
  if (!this.connected || !this.ws)
@@ -43407,6 +43461,10 @@ class FrontendWebSocket {
43407
43461
  return this.completion(todoId);
43408
43462
  }
43409
43463
  completion(todoId) {
43464
+ if (!this.connected && !this.completionResults.has(todoId)) {
43465
+ this.callbacks.delete(todoId);
43466
+ return Promise.resolve({ type: "socket:closed", payload: { todoId, status: "DISCONNECTED" }, success: false });
43467
+ }
43410
43468
  const early = this.completionResults.get(todoId);
43411
43469
  if (early) {
43412
43470
  this.completionResults.delete(todoId);
@@ -43782,6 +43840,7 @@ var SERVER_TO_FRONTENDS = {
43782
43840
  msgMetaAi: (todoId) => `${"todo:msg_meta_ai" /* TODO_MSG_META_AI */}:${todoId}`,
43783
43841
  msgQueueUpdate: (todoId) => `${"todo:msg_queue_update" /* TODO_MSG_QUEUE_UPDATE */}:${todoId}`,
43784
43842
  message_removed: (todoId) => `${"todo:message_removed" /* TODO_MESSAGE_REMOVED */}:${todoId}`,
43843
+ commentUpdate: (todoId) => `${"todo:comment_update" /* TODO_COMMENT_UPDATE */}:${todoId}`,
43785
43844
  start_universal: (todoId) => `${"block:start_universal" /* BLOCK_START_UNIVERSAL */}:${todoId}`,
43786
43845
  message: (todoId) => `${"block:message" /* BLOCK_MESSAGE */}:${todoId}`,
43787
43846
  end: (todoId) => `${"block:end" /* BLOCK_END */}:${todoId}`,
@@ -43823,6 +43882,7 @@ function manualAnswersOf(sources) {
43823
43882
  return out;
43824
43883
  }
43825
43884
  var manualSamples = (answers) => Object.entries(answers).filter(([, a]) => a.trim()).map(([prompt, text]) => ({ prompt, text: text.trim() }));
43885
+ var senderLabel = (s) => s.kind === "team" ? `${s.name} · TODO for AI team` : `${s.name} · your agent`;
43826
43886
  // ../packages/shared-fbe/src/templateBody.ts
43827
43887
  var USER_HEADINGS = new Set([
43828
43888
  "your task",
@@ -44332,6 +44392,1499 @@ var OUTPUT_POLICIES = {
44332
44392
  full: { firstLimit: Infinity, lastLimit: 0, hardCap: RUN_OUTPUT_CAP, lineLimit: MAX_LINE_LEN },
44333
44393
  raw: { firstLimit: Infinity, lastLimit: 0, hardCap: Infinity, lineLimit: Infinity }
44334
44394
  };
44395
+ // ../packages/shared-fbe/src/tool_catalog.json
44396
+ var tool_catalog_default = {
44397
+ zele: {
44398
+ category: "email",
44399
+ pkg: "zele",
44400
+ installer: "npm",
44401
+ label: "Gmail",
44402
+ summary: "Read and send Gmail from your own account.",
44403
+ statusCmd: "zele whoami 2>/dev/null | grep 'email:' | grep -m1 .",
44404
+ loginNeedsPty: true,
44405
+ loginCmd: "zele login --method google",
44406
+ oauthUrlPattern: "(https://accounts\\.google\\.com\\S+)",
44407
+ oauthCallbackPort: 8089,
44408
+ credentialPaths: ["~/.zele/sqlite.db"],
44409
+ versionCmd: "zele --version 2>/dev/null | sed -n 1p",
44410
+ preinstallCloud: true,
44411
+ preinstallJarvis: true,
44412
+ description: "Gmail (multi-account, OAuth login): inbox, search, send, reply, contacts. Google Calendar via `zele cal`: list/create/update/delete events (Meet links, recurrence), respond to invites, free/busy."
44413
+ },
44414
+ "zele-calendar": {
44415
+ preinstallCloud: true,
44416
+ category: "utility",
44417
+ pkg: "zele",
44418
+ installer: "npm",
44419
+ binName: "zele",
44420
+ label: "Google Calendar",
44421
+ summary: "See your Google Calendar and book, move or cancel events.",
44422
+ statusCmd: "zele whoami 2>/dev/null | grep 'email:' | grep -m1 .",
44423
+ loginNeedsPty: true,
44424
+ loginCmd: "zele login --method google",
44425
+ oauthUrlPattern: "(https://accounts\\.google\\.com\\S+)",
44426
+ oauthCallbackPort: 8089,
44427
+ credentialPaths: ["~/.zele/sqlite.db"],
44428
+ versionCmd: "zele --version 2>/dev/null | sed -n 1p",
44429
+ preinstallJarvis: true,
44430
+ description: "Google Calendar via `zele cal`: list calendars and events, create/update/delete events with Google Meet links, recurrence and reminders, respond to invites, free/busy."
44431
+ },
44432
+ xurl: {
44433
+ category: "social",
44434
+ pkg: "@xdevplatform/xurl",
44435
+ installer: "npm",
44436
+ label: "X",
44437
+ summary: "Post to X and read timelines, replies and DMs.",
44438
+ statusCmd: `xurl whoami 2>&1 | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
44439
+ loginCmd: "xurl auth oauth2",
44440
+ loginTerminalHint: "X needs your own developer app first: create one at developer.x.com (callback `http://localhost:8080/callback`), run `xurl auth apps add tfa --client-id <id> --client-secret <secret>`, then `xurl auth oauth2`.",
44441
+ oauthUrlPattern: "(https://(?:twitter|x)\\.com/i/oauth2\\S+)",
44442
+ oauthCallbackPort: 8080,
44443
+ credentialPaths: ["~/.xurl"],
44444
+ description: "Raw X API access via `xurl <method> <path>`.",
44445
+ versionCmd: "xurl version 2>/dev/null | sed -n 1p",
44446
+ preinstallJarvis: true,
44447
+ preinstallCloud: true
44448
+ },
44449
+ "linkedin-api": {
44450
+ preinstallCloud: true,
44451
+ category: "social",
44452
+ pkg: "@todoforai/linkedin-api",
44453
+ installer: "npm",
44454
+ label: "LinkedIn",
44455
+ summary: "Read LinkedIn profiles and send messages and connection requests.",
44456
+ statusCmd: `linkedin-api whoami 2>/dev/null | sed -n 's/.*"publicId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
44457
+ loginCmd: "linkedin-api auth <li_at> <jsessionid>",
44458
+ credentialPaths: ["~/.config/linkedin-api/credentials.json"],
44459
+ description: "LinkedIn Voyager API CLI for profiles, conversations, DMs, and connections.",
44460
+ versionCmd: "linkedin-api --version 2>/dev/null | sed -n 1p"
44461
+ },
44462
+ "figma-api": {
44463
+ category: "design",
44464
+ pkg: "@todoforai/figma-api",
44465
+ installer: "npm",
44466
+ label: "Figma",
44467
+ summary: "Read Figma files, export images and leave comments.",
44468
+ statusCmd: `figma-api me 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
44469
+ loginCmd: "figma-api auth <token>",
44470
+ credentialPaths: ["~/.config/figma-api/credentials.json"],
44471
+ description: "Figma REST API CLI. File commands take a file key or Figma URL. Canvas writes (frames/text/shapes) use `figma-api run` via the plugin bridge. Run `figma-api <cmd> --help` for params.",
44472
+ versionCmd: "figma-api --version 2>/dev/null | sed -n 1p"
44473
+ },
44474
+ "slack-cli": {
44475
+ preinstallCloud: true,
44476
+ cloudInstallCmd: "curl -fsSL https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_amd64.tar.gz | tar -xz -C /tmp bin/slack && install -m0755 /tmp/bin/slack /usr/local/bin/slack-cli && rm -rf /tmp/bin",
44477
+ cloudVerifyCmd: "slack-cli --version",
44478
+ category: "social",
44479
+ pkg: "slack-cli",
44480
+ installer: "binary",
44481
+ label: "Slack",
44482
+ summary: "Work with Slack workspaces and apps.",
44483
+ statusCmd: "slack-cli auth list 2>&1 | grep 'Team' >/dev/null && echo authenticated",
44484
+ loginCmd: "slack-cli login",
44485
+ loginTerminalHint: "Run `slack-cli login` in a terminal on this host, paste the `/slackauthticket …` command it prints into any Slack channel, then type the challenge code Slack shows you back into the terminal.",
44486
+ credentialPaths: ["~/.slack/credentials.json"],
44487
+ description: "Invoked as `slack-cli` to avoid colliding with the Slack desktop app’s `slack` binary.",
44488
+ versionCmd: "slack-cli version 2>/dev/null | sed -n 1p",
44489
+ binName: "slack-cli",
44490
+ binary: {
44491
+ "linux-x86_64": {
44492
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_amd64.tar.gz",
44493
+ archive: "tar.gz",
44494
+ extract: "bin/slack"
44495
+ },
44496
+ "linux-aarch64": {
44497
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_arm64.tar.gz",
44498
+ archive: "tar.gz",
44499
+ extract: "bin/slack"
44500
+ },
44501
+ "darwin-x86_64": {
44502
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_amd64.tar.gz",
44503
+ archive: "tar.gz",
44504
+ extract: "bin/slack"
44505
+ },
44506
+ "darwin-aarch64": {
44507
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_arm64.tar.gz",
44508
+ archive: "tar.gz",
44509
+ extract: "bin/slack"
44510
+ },
44511
+ "windows-x86_64": {
44512
+ url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_windows_64-bit.zip",
44513
+ archive: "zip",
44514
+ extract: "bin/slack.exe"
44515
+ }
44516
+ }
44517
+ },
44518
+ "telegram-send": {
44519
+ preinstallCloud: true,
44520
+ packages: ["telegram-send==0.39.2"],
44521
+ category: "social",
44522
+ pkg: "telegram-send",
44523
+ installer: "pip",
44524
+ label: "Telegram",
44525
+ summary: "Send Telegram messages, photos and files.",
44526
+ loginCmd: `t=<bot_token>; c=$(curl -sf "https://api.telegram.org/bot$t/getUpdates" | python3 -c "import json,sys; u=[x for x in json.load(sys.stdin)['result'] if 'message' in x]; print(u[-1]['message']['chat']['id'])" 2>/dev/null) || { echo "No message from you yet: open your bot in Telegram, send it any message, then retry"; exit 1; }; mkdir -p "$HOME/.config" && printf '[telegram]\\ntoken = %s\\nchat_id = %s\\n' "$t" "$c" > "$HOME/.config/telegram-send.conf" && telegram-send "Connected to TODOforAI"`,
44527
+ credentialPaths: ["~/.config/telegram-send.conf"],
44528
+ versionCmd: "telegram-send --version 2>/dev/null | sed -n 1p",
44529
+ statusCmd: "test -f ~/.config/telegram-send.conf && echo authenticated",
44530
+ description: "Send messages, send files & images, send video & audio, Markdown/HTML formatting, channel & group support"
44531
+ },
44532
+ "meta-ads": {
44533
+ preinstallCloud: true,
44534
+ packages: ["meta-ads==1.1.0"],
44535
+ category: "marketing",
44536
+ pkg: "meta-ads",
44537
+ installer: "pip",
44538
+ label: "Meta Ads",
44539
+ summary: "Run Facebook and Instagram ad campaigns and see how they perform.",
44540
+ binName: "meta",
44541
+ statusCmd: `out=$(meta auth status 2>&1); rc=$?; printf '%s\\n' "$out" | grep -m1 .; exit $rc`,
44542
+ versionCmd: "meta --version 2>/dev/null | sed -n 1p",
44543
+ description: "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. No raw/passthrough mode; for arbitrary Graph API calls with a Page token use `facebook-pages-api request <method> <path>`."
44544
+ },
44545
+ "elevenlabs-api": {
44546
+ preinstallCloud: true,
44547
+ category: "media",
44548
+ pkg: "@todoforai/elevenlabs-api",
44549
+ installer: "npm",
44550
+ label: "ElevenLabs",
44551
+ summary: "Turn text into natural speech, in many voices and languages.",
44552
+ versionCmd: "elevenlabs-api --version 2>/dev/null | sed -n 1p",
44553
+ statusCmd: 'test -f ~/.config/todoforai/elevenlabs.json -o -n "$ELEVENLABS_API_KEY" && echo authenticated',
44554
+ loginCmd: "elevenlabs-api auth <api_key>",
44555
+ credentialPaths: ["~/.config/todoforai/elevenlabs.json"],
44556
+ description: "Text-to-speech, voice cloning, multiple languages."
44557
+ },
44558
+ ffmpeg: {
44559
+ commodity: true,
44560
+ category: "media",
44561
+ pkg: "ffmpeg",
44562
+ installer: "system",
44563
+ label: "FFmpeg",
44564
+ summary: "Convert, trim and resize video and audio files.",
44565
+ cloudAptPackages: ["ffmpeg"],
44566
+ preinstallCloud: true,
44567
+ description: "Media swiss-army knife: `ffmpeg -i in.mp4 out.webm`; `ffprobe` for metadata.",
44568
+ versionCmd: "ffmpeg -version 2>/dev/null | sed -n 1p"
44569
+ },
44570
+ "tfa-imagegen": {
44571
+ preinstallCloud: true,
44572
+ category: "media",
44573
+ pkg: "@todoforai/image-api",
44574
+ installer: "npm",
44575
+ label: "Image Gen",
44576
+ summary: "Generate and edit images from a description.",
44577
+ platformAuth: true,
44578
+ versionCmd: "tfa-imagegen --version 2>/dev/null | sed -n 1p",
44579
+ credentialPaths: ["~/.config/todoforai/credentials.json"],
44580
+ description: "AI image generation & editing via TODOFORAI backend (gpt-image, nano-banana, nano-banana-pro, mai-image-2.6)"
44581
+ },
44582
+ "suno-api": {
44583
+ preinstallCloud: true,
44584
+ category: "media",
44585
+ pkg: "@todoforai/suno-api",
44586
+ installer: "npm",
44587
+ label: "Suno",
44588
+ summary: "Generate music and songs from a prompt.",
44589
+ versionCmd: "suno-api --version 2>/dev/null | sed -n 1p",
44590
+ statusCmd: 'test -f ~/.config/todoforai/suno.json -o -n "$SUNO_API_KEY" && echo authenticated',
44591
+ loginCmd: "suno-api auth <api_key>",
44592
+ credentialPaths: [
44593
+ "~/.config/todoforai/suno.json"
44594
+ ],
44595
+ description: "AI music generation, custom lyrics, multiple genres."
44596
+ },
44597
+ ntn: {
44598
+ preinstallCloud: true,
44599
+ category: "utility",
44600
+ pkg: "ntn",
44601
+ installer: "npm",
44602
+ label: "Notion",
44603
+ summary: "Read and update Notion pages and databases.",
44604
+ statusCmd: "ntn doctor 2>&1 | grep -E 'Token valid\\s+✔' >/dev/null && echo authenticated",
44605
+ loginCmd: "ntn login",
44606
+ loginNeedsPty: true,
44607
+ oauthUrlPattern: "(https://www\\.notion\\.so/workers/cli-login\\S+)",
44608
+ sessionIdPattern: "verificationCode=([A-Z0-9-]{5,12})",
44609
+ credentialPaths: ["~/.config/notion"],
44610
+ versionCmd: "ntn --version 2>/dev/null | sed -n 1p",
44611
+ description: "Official Notion CLI: raw API calls (`ntn api <path>`), page/data-source management, file uploads, Workers deploy."
44612
+ },
44613
+ "perplexity-api": {
44614
+ preinstallCloud: true,
44615
+ category: "utility",
44616
+ pkg: "@todoforai/perplexity-api",
44617
+ installer: "npm",
44618
+ label: "Perplexity",
44619
+ summary: "Ask questions and get answers researched from the web.",
44620
+ versionCmd: "perplexity-api --version 2>/dev/null | sed -n 1p",
44621
+ statusCmd: 'test -f ~/.config/todoforai/perplexity.json && echo authenticated || test -n "$PERPLEXITY_API_KEY" && echo authenticated',
44622
+ loginCmd: "perplexity-api auth <api_key>",
44623
+ credentialPaths: ["~/.config/todoforai/perplexity.json"],
44624
+ description: "AI-powered web search, chat completions, async chat, embeddings and agent runs"
44625
+ },
44626
+ gh: {
44627
+ category: "development",
44628
+ pkg: "gh",
44629
+ installer: "binary",
44630
+ preinstallCloud: true,
44631
+ label: "GitHub",
44632
+ summary: "Work with your GitHub repos, pull requests and issues.",
44633
+ statusCmd: "gh auth status 2>&1 | grep 'Logged in to' | grep -m1 .",
44634
+ loginCmd: "gh auth login --hostname github.com --git-protocol https",
44635
+ oauthUrlPattern: "(https://github\\.com/login/device\\S*)",
44636
+ deviceCodePattern: "one-time code:\\s*([A-Z0-9]{4}-[A-Z0-9]{4})",
44637
+ canStreamLogin: true,
44638
+ logoutCmd: "gh auth logout --hostname github.com --user {{ACCOUNT}}",
44639
+ credentialPaths: ["~/.config/gh/hosts.yml"],
44640
+ description: "Prefer `gh` over raw GitHub API calls.",
44641
+ versionCmd: "gh --version 2>/dev/null | sed -n 1p",
44642
+ preinstallJarvis: true
44643
+ },
44644
+ mcporter: {
44645
+ category: "development",
44646
+ pkg: "mcporter",
44647
+ installer: "npm",
44648
+ label: "MCP (mcporter)",
44649
+ summary: "Use tools from any MCP server.",
44650
+ statusCmd: "mcporter list 2>/dev/null | grep -v 'No MCP servers' | grep -m1 .",
44651
+ description: "MCP servers as CLI: `mcporter list [server] [--schema]`, `mcporter call <server>.<tool> key=value` (`key=@file`), `mcporter config add <name> <cmd-or-url>`, `mcporter auth <server>`. `--help` for more.",
44652
+ versionCmd: "mcporter --version 2>/dev/null | sed -n 1p",
44653
+ preinstallJarvis: true,
44654
+ preinstallCloud: true
44655
+ },
44656
+ glab: {
44657
+ category: "development",
44658
+ pkg: "glab",
44659
+ installer: "binary",
44660
+ label: "GitLab",
44661
+ summary: "Work with your GitLab repos, merge requests and pipelines.",
44662
+ statusCmd: "glab auth status 2>&1 | grep 'Logged in to' | grep -m1 .",
44663
+ loginCmd: "glab auth login --device",
44664
+ credentialPaths: ["~/.config/glab-cli/config.yml"],
44665
+ description: "GitLab equivalent of `gh`.",
44666
+ versionCmd: "glab --version 2>/dev/null | sed -n 1p",
44667
+ deviceCodePattern: "one-time code: ([A-Z0-9-]+)",
44668
+ oauthUrlPattern: "(https://gitlab\\.com/oauth/device)",
44669
+ canStreamLogin: true
44670
+ },
44671
+ vercel: {
44672
+ preinstallCloud: true,
44673
+ category: "deployment",
44674
+ pkg: "vercel",
44675
+ installer: "npm",
44676
+ label: "Vercel",
44677
+ summary: "Deploy websites to Vercel and manage them.",
44678
+ statusCmd: "vercel whoami 2>/dev/null | tr -d '\\r' | grep -m1 .",
44679
+ loginCmd: "vercel login",
44680
+ credentialPaths: ["~/.local/share/com.vercel.cli/auth.json"],
44681
+ description: "`vercel deploy`; manage env vars, domains, preview URLs.",
44682
+ versionCmd: "vercel --version 2>/dev/null | sed -n 1p",
44683
+ deviceCodePattern: "user_code=([A-Z0-9-]+)",
44684
+ oauthUrlPattern: "(https://vercel\\.com/oauth/device\\S*)",
44685
+ canStreamLogin: true
44686
+ },
44687
+ eas: {
44688
+ category: "deployment",
44689
+ pkg: "eas-cli",
44690
+ installer: "npm",
44691
+ label: "Expo EAS",
44692
+ summary: "Build, update and submit mobile apps with Expo.",
44693
+ statusCmd: `out=$(eas whoami 2>/dev/null) && printf '%s\\n' "$out" | tr -d '\\r' | grep -m1 .`,
44694
+ loginCmd: "eas login --browser",
44695
+ logoutCmd: "eas logout",
44696
+ oauthUrlPattern: "(https://expo\\.dev/(?:sso-)?login\\?\\S+)",
44697
+ canStreamLogin: true,
44698
+ credentialPaths: ["~/.expo/state.json"],
44699
+ description: "Expo Application Services for React Native apps: `eas update --channel <c>` ships a JS-only OTA, `eas build -p ios --profile production --auto-submit` (`-p all` for both) makes a native build and sends it to TestFlight / Play. `eas build:list`, `eas update:list` and `eas submit` inspect or re-submit existing builds. An OTA only reaches installs whose runtimeVersion matches, so a native version bump needs a real build. The browser login binds to a RANDOM loopback port, so it only completes in a browser on the same host; on a remote/headless host set EXPO_TOKEN (Account -> Access tokens) instead. EXPO_TOKEN (Account -> Access tokens) works in CI instead of a login.",
44700
+ versionCmd: "eas --version 2>/dev/null | sed -n 1p"
44701
+ },
44702
+ netlify: {
44703
+ category: "deployment",
44704
+ pkg: "netlify-cli",
44705
+ installer: "npm",
44706
+ label: "Netlify",
44707
+ summary: "Deploy websites to Netlify and manage them.",
44708
+ statusCmd: `netlify api getCurrentUser 2>&1 | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
44709
+ loginCmd: "netlify login",
44710
+ credentialPaths: ["~/.netlify/config.json"],
44711
+ description: "`netlify deploy --prod` for production; manage env vars, forms, identity.",
44712
+ versionCmd: "netlify --version 2>/dev/null | sed -n 1p",
44713
+ oauthUrlPattern: "(https://app\\.netlify\\.com/authorize\\S+)",
44714
+ canStreamLogin: true
44715
+ },
44716
+ firebase: {
44717
+ preinstallCloud: true,
44718
+ category: "deployment",
44719
+ pkg: "firebase-tools",
44720
+ installer: "npm",
44721
+ label: "Firebase",
44722
+ summary: "Deploy and manage Firebase hosting, database and functions.",
44723
+ statusCmd: "firebase login:list 2>&1 | grep -oiE '[[:alnum:]._%+-]+@[[:alnum:].-]+\\.[[:alpha:]]{2,}' | grep -m1 .",
44724
+ loginCmd: "firebase login --no-localhost --reauth",
44725
+ loginNeedsPty: true,
44726
+ stdinPaste: true,
44727
+ credentialPaths: ["~/.config/configstore/firebase-tools.json"],
44728
+ description: "Deploy hosting/functions, manage Firestore/RTDB rules and data, auth users, emulators.",
44729
+ versionCmd: "firebase --version 2>/dev/null | sed -n 1p",
44730
+ oauthUrlPattern: "(https://auth\\.firebase\\.tools/login\\S+)",
44731
+ sessionIdPattern: "session ID:\\s*([0-9A-Za-z]{4,8})"
44732
+ },
44733
+ wrangler: {
44734
+ preinstallCloud: true,
44735
+ category: "deployment",
44736
+ pkg: "wrangler",
44737
+ installer: "npm",
44738
+ label: "Cloudflare",
44739
+ summary: "Deploy and manage Cloudflare Workers and Pages.",
44740
+ statusCmd: "wrangler whoami 2>&1 | grep -oiE '[[:alnum:]._%+-]+@[[:alnum:].-]+\\.[[:alpha:]]{2,}' | grep -m1 .",
44741
+ loginCmd: "wrangler login",
44742
+ credentialPaths: ["~/.config/.wrangler/config/default.toml"],
44743
+ description: "Tail logs with `wrangler tail`. Use `cloudflared` for tunnels.",
44744
+ versionCmd: "wrangler --version 2>/dev/null | sed -n 1p",
44745
+ oauthUrlPattern: "(https://dash\\.cloudflare\\.com/oauth2/auth\\S+)",
44746
+ oauthCallbackPort: 8976
44747
+ },
44748
+ stripe: {
44749
+ preinstallCloud: true,
44750
+ cloudInstallCmd: "curl -fsSL https://github.com/stripe/stripe-cli/releases/download/v1.50.10/stripe_1.50.10_linux_x86_64.tar.gz | tar -xz -C /usr/local/bin stripe",
44751
+ cloudVerifyCmd: "stripe --version",
44752
+ category: "payments",
44753
+ pkg: "stripe",
44754
+ installer: "binary",
44755
+ label: "Stripe",
44756
+ summary: "Look up Stripe payments, customers and subscriptions.",
44757
+ statusCmd: "stripe config --list 2>/dev/null | sed -n 's/^account_id[[:space:]]*=[[:space:]]*.\\([^'\\'']*\\).*/\\1/p' | grep -m10 .",
44758
+ loginCmd: "stripe login",
44759
+ loginNeedsPty: true,
44760
+ credentialPaths: ["~/.config/stripe/config.toml"],
44761
+ description: "`stripe listen` forwards webhooks locally; `stripe get/post /v1/...` for raw API.",
44762
+ versionCmd: "stripe --version 2>/dev/null | sed -n 1p",
44763
+ deviceCodePattern: "verification code:\\s*([A-Z0-9]{4}-[A-Z0-9]{4})",
44764
+ oauthUrlPattern: "(https://access\\.stripe\\.com/stripecli/\\S+)",
44765
+ canStreamLogin: true
44766
+ },
44767
+ flyctl: {
44768
+ category: "cloud",
44769
+ pkg: "flyctl",
44770
+ installer: "binary",
44771
+ label: "Fly.io",
44772
+ summary: "Deploy and scale apps on Fly.io.",
44773
+ statusCmd: "flyctl auth whoami 2>/dev/null | tr -d '\\r' | grep -m1 .",
44774
+ loginCmd: "flyctl auth login -t <access_token>",
44775
+ credentialPaths: ["~/.fly/config.yml"],
44776
+ description: "`flyctl deploy` from app dir; manage Postgres/volumes/machines/secrets.",
44777
+ versionCmd: "flyctl version 2>/dev/null | sed -n 1p"
44778
+ },
44779
+ supabase: {
44780
+ preinstallCloud: true,
44781
+ cloudInstallCmd: "curl -fsSL https://github.com/supabase/cli/releases/download/v2.117.0/supabase_2.117.0_linux_amd64.tar.gz | tar -xz -C /usr/local/bin supabase",
44782
+ cloudVerifyCmd: "supabase --version",
44783
+ category: "cloud",
44784
+ pkg: "supabase",
44785
+ installer: "binary",
44786
+ label: "Supabase",
44787
+ summary: "Manage your Supabase database, auth and storage.",
44788
+ statusCmd: "supabase projects list >/dev/null 2>&1 && echo authenticated",
44789
+ loginCmd: "supabase login --token <access_token>",
44790
+ credentialPaths: ["~/.config/supabase/access-token"],
44791
+ description: "Link to cloud project; runs/migrates local Postgres.",
44792
+ versionCmd: "supabase --version 2>/dev/null | sed -n 1p"
44793
+ },
44794
+ railway: {
44795
+ category: "cloud",
44796
+ pkg: "@railway/cli",
44797
+ installer: "npm",
44798
+ label: "Railway",
44799
+ summary: "Deploy and manage apps and databases on Railway.",
44800
+ statusCmd: `out=$(RAILWAY_NO_TELEMETRY=1 railway whoami 2>&1); rc=$?; printf '%s\\n' "$out" | grep -m1 .; exit $rc`,
44801
+ loginCmd: "railway login --browserless",
44802
+ credentialPaths: ["~/.railway/config.json"],
44803
+ description: "`railway up` to deploy current dir.",
44804
+ versionCmd: "railway --version 2>/dev/null | sed -n 1p",
44805
+ deviceCodePattern: "user_code=([A-Z0-9-]+)",
44806
+ oauthUrlPattern: "(https://railway\\.com/activate\\S*)",
44807
+ canStreamLogin: true
44808
+ },
44809
+ shopify: {
44810
+ preinstallCloud: true,
44811
+ category: "ecommerce",
44812
+ pkg: "@shopify/cli",
44813
+ installer: "npm",
44814
+ label: "Shopify",
44815
+ summary: "Build and manage Shopify stores and themes.",
44816
+ loginCmd: "shopify auth login --alias tfa",
44817
+ credentialPaths: ["~/.config/shopify-cli-kit-nodejs/config.json"],
44818
+ versionCmd: `node -p "require(require('fs').realpathSync(require('child_process').execSync('command -v shopify',{encoding:'utf8'}).trim()).replace(/\\/bin\\/[^/]*$/, '/package.json')).version" 2>/dev/null`,
44819
+ statusCmd: `python3 -c "import json,os,sys;p=os.path.expanduser('~/.config/shopify-cli-kit-nodejs/config.json');sys.exit(0 if json.load(open(p)).get('session') else 1)" 2>/dev/null && echo authenticated`,
44820
+ deviceCodePattern: "verification code: ([A-Z0-9-]+)",
44821
+ oauthUrlPattern: "(https://accounts\\.shopify\\.com/activate-with-code\\S*)",
44822
+ canStreamLogin: true,
44823
+ description: "Theme development, app scaffolding, store management"
44824
+ },
44825
+ "shop-app": {
44826
+ preinstallCloud: true,
44827
+ category: "ecommerce",
44828
+ pkg: "@todoforai/shop-app",
44829
+ installer: "npm",
44830
+ label: "Shop",
44831
+ summary: "Search products and compare prices across Shopify stores.",
44832
+ description: "Shop.app product search across Shopify stores. `shop-app search '<query>' --ships-to HU -n 10` returns products with price, rating, URL, and variants. `shop-app similar <variantId>` finds lookalikes.",
44833
+ versionCmd: "shop-app --version 2>/dev/null | sed -n 1p"
44834
+ },
44835
+ "datadog-ci": {
44836
+ category: "monitoring",
44837
+ pkg: "@datadog/datadog-ci",
44838
+ installer: "npm",
44839
+ label: "Datadog",
44840
+ summary: "Send test results and releases to Datadog.",
44841
+ versionCmd: "datadog-ci version 2>/dev/null | sed -n 1p",
44842
+ statusCmd: 'test -n "$DATADOG_API_KEY" && echo authenticated',
44843
+ description: "CI test visibility, sourcemap uploads, deployment tracking."
44844
+ },
44845
+ "sentry-cli": {
44846
+ preinstallCloud: true,
44847
+ category: "monitoring",
44848
+ pkg: "@sentry/cli",
44849
+ installer: "npm",
44850
+ label: "Sentry",
44851
+ summary: "Manage Sentry releases and error tracking.",
44852
+ statusCmd: "sentry-cli info --no-defaults >/dev/null 2>&1 && echo authenticated",
44853
+ loginCmd: "sentry-cli login --auth-token <auth_token>",
44854
+ credentialPaths: ["~/.sentryclirc"],
44855
+ description: "`sentry-cli releases new <version>`.",
44856
+ versionCmd: "sentry-cli --version 2>/dev/null | sed -n 1p"
44857
+ },
44858
+ "tfa-cli": {
44859
+ category: "utility",
44860
+ pkg: "@todoforai/cli",
44861
+ installer: "npm",
44862
+ label: "TODOforAI",
44863
+ summary: "Create and follow up on your TODOs, and show results in the chat.",
44864
+ description: 'Manage the user\'s OWN TODOforAI tasks (not code `// TODO` comments). Delegate/spin off work: `tfa-cli "prompt"` creates a real TODO with its own agent and chat (`-n` wait for completion, `--no-watch` fire-and-forget, `--project/--agent/--model`, `--resume <id>`, `addmessage <id> "text"`). Inspect: `list [--status open]`, `--inspect <id>`. Edit any todo without opening it: `status <id> DONE|READY|ARCHIVED|POSTPONED|CANCELLED|DELETED` (close, reopen, shelve, trash — `todo restore <id>` undoes a delete) and `todo set <id|-> title=… group=… star=true schedule=<unix-ms> agent=… brand=…` (own todo fields). Both take ONE id, so a loop over `list --json | jq -r \'.items[].id\'` is how you close, retitle, refile or archive MANY todos at once; `project set|settings|groups …` (name, settings, todo groups), `agent create|update <agent> model=… sysmsg=…` (permissions/MCP/device wiring and delete are refused from an agent shell). Surface results into chat: `show <file|-> [todo-id]` (renders by mimetype; `--card <name>` via project renderer; `--alias <name>` updates in place), `open <url>` embeds a live preview. Notifications: `inbox [needs-you|messages|activity] [--unread]` reads the user\'s bell feed (what finished/failed/needs them while away), `inbox seen [tab]` marks it read; `notify "Title" "message" [--href /t/<id>]` leaves a note in the user\'s Messages tab + phone push — recipient is ALWAYS the key\'s own user (no --to; cannot message teammates or anyone else), shown as from you, same text is delivered once, max 10/day, use for things they must see later (not chat replies). This is the ONLY way to display a local file (image, pdf, html) in the chat: markdown `![](path.png)` with a local/relative path does NOT resolve to workspace files or attachments and 404s in the browser. `--template` for registry workflows.',
44865
+ installCmd: "npm install -g @todoforai/cli",
44866
+ versionCmd: "tfa-cli --version 2>/dev/null | sed -n 1p",
44867
+ preinstallCloud: true,
44868
+ preinstallJarvis: true,
44869
+ aliases: ["todoforai-cli", "todoai"]
44870
+ },
44871
+ curl: {
44872
+ commodity: true,
44873
+ category: "networking",
44874
+ pkg: "curl",
44875
+ installer: "system",
44876
+ label: "Web Request",
44877
+ summary: "Fetch URLs and call web APIs.",
44878
+ binName: "curl",
44879
+ description: "HTTP/HTTPS/FTP client: GET/POST/PUT/DELETE, headers, auth, file upload/download, follow redirects, cookies, TLS.",
44880
+ versionCmd: "curl --version 2>/dev/null | sed -n 1p | cut -d' ' -f1-2",
44881
+ preinstallCloud: true,
44882
+ internal: true,
44883
+ preinstallJarvis: true
44884
+ },
44885
+ cloudflared: {
44886
+ preinstallCloud: true,
44887
+ cloudInstallCmd: "curl -fsSL https://github.com/cloudflare/cloudflared/releases/download/2026.9.0/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod 0755 /usr/local/bin/cloudflared",
44888
+ cloudVerifyCmd: "cloudflared --version",
44889
+ commodity: true,
44890
+ category: "networking",
44891
+ pkg: "cloudflared",
44892
+ installer: "binary",
44893
+ label: "Tunnel",
44894
+ summary: "Put a local app on a public web address.",
44895
+ description: "`cloudflared tunnel --url http://localhost:<port>` prints a public https URL.",
44896
+ versionCmd: "cloudflared --version 2>/dev/null | sed -n 1p"
44897
+ },
44898
+ vault: {
44899
+ category: "security",
44900
+ pkg: "vault",
44901
+ installer: "binary",
44902
+ label: "HashiCorp Vault",
44903
+ summary: "Read and manage secrets in HashiCorp Vault.",
44904
+ statusCmd: "vault token lookup >/dev/null 2>&1 && echo authenticated",
44905
+ loginCmd: "vault login <token>",
44906
+ credentialPaths: ["~/.vault-token"],
44907
+ versionCmd: "vault --version 2>/dev/null | sed -n '1s/ (.*//p'",
44908
+ description: "HashiCorp Vault secrets management and dynamic credentials."
44909
+ },
44910
+ rclone: {
44911
+ category: "cloud",
44912
+ pkg: "rclone",
44913
+ installer: "binary",
44914
+ preinstallCloud: true,
44915
+ binary: {
44916
+ "linux-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-linux-amd64.tar.gz", archive: "tar.gz", extract: "rclone" },
44917
+ "linux-aarch64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-linux-arm64.tar.gz", archive: "tar.gz", extract: "rclone" },
44918
+ "darwin-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-darwin-amd64.tar.gz", archive: "tar.gz", extract: "rclone" },
44919
+ "darwin-aarch64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-darwin-arm64.tar.gz", archive: "tar.gz", extract: "rclone" },
44920
+ "windows-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-windows-amd64.zip", archive: "zip", extract: "rclone.exe" }
44921
+ },
44922
+ label: "Cloud Storage",
44923
+ summary: "Move files between Google Drive, Dropbox, OneDrive, S3 and more.",
44924
+ statusCmd: "rclone listremotes --long 2>/dev/null | tr -s ' ' | grep -v ' todoforai *$' | grep -m20 .",
44925
+ loginCmd: "rclone config",
44926
+ loginTerminalHint: "Connect a storage from its own tile (Google Drive, OneDrive, Dropbox…). For other backends run `rclone config` in a terminal on this host.",
44927
+ oauthUrlPattern: "(https?://127\\.0\\.0\\.1:\\d+/auth\\?\\S+)",
44928
+ oauthCallbackPort: 53682,
44929
+ credentialPaths: ["~/.config/rclone/rclone.conf"],
44930
+ subProviders: {
44931
+ gdrive: {
44932
+ label: "Google Drive",
44933
+ authType: "oauth",
44934
+ connectCmd: "rclone config create {{REMOTE}} drive",
44935
+ remoteName: "gdrive",
44936
+ rcloneType: "drive",
44937
+ userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf 'https://www.googleapis.com/drive/v3/about?fields=user' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['user']['emailAddress'])" 2>/dev/null`
44938
+ },
44939
+ onedrive: {
44940
+ label: "OneDrive",
44941
+ authType: "oauth",
44942
+ connectCmd: "rclone config create {{REMOTE}} onedrive",
44943
+ remoteName: "onedrive",
44944
+ rcloneType: "onedrive",
44945
+ userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf 'https://graph.microsoft.com/v1.0/me' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('mail') or d.get('userPrincipalName',''))" 2>/dev/null`
44946
+ },
44947
+ dropbox: {
44948
+ label: "Dropbox",
44949
+ authType: "oauth",
44950
+ connectCmd: "rclone config create {{REMOTE}} dropbox",
44951
+ remoteName: "dropbox",
44952
+ rcloneType: "dropbox",
44953
+ userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf -X POST 'https://api.dropboxapi.com/2/users/get_current_account' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['email'])" 2>/dev/null`
44954
+ },
44955
+ s3: {
44956
+ label: "Amazon S3",
44957
+ authType: "credentials",
44958
+ connectCmd: "rclone config create {{REMOTE}} s3",
44959
+ remoteName: "s3",
44960
+ rcloneType: "s3"
44961
+ }
44962
+ },
44963
+ versionCmd: "rclone version 2>/dev/null | sed -n 1p",
44964
+ preinstallJarvis: true,
44965
+ description: "Access Google Drive, OneDrive, Dropbox, S3, and 40+ cloud providers. List, copy, sync, move, and mount files; use `rclone config create <name> <provider>` to connect."
44966
+ },
44967
+ aws: {
44968
+ preinstallCloud: true,
44969
+ cloudInstallCmd: "curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o /tmp/awscli.zip && unzip -qo /tmp/awscli.zip -d /tmp && /tmp/aws/install --update && rm -rf /tmp/aws /tmp/awscli.zip",
44970
+ cloudVerifyCmd: "aws --version",
44971
+ category: "cloud",
44972
+ pkg: "awscli",
44973
+ installer: "system",
44974
+ label: "AWS",
44975
+ summary: "Manage your AWS account and services.",
44976
+ installCmd: `mkdir -p "$HOME/.todoforai/tools/bin"; if [ "$(uname -s)" = Darwin ]; then curl -fsSL https://awscli.amazonaws.com/AWSCLIV2.pkg -o /tmp/AWSCLIV2.pkg && printf '<?xml version="1.0" encoding="UTF-8"?>\\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\\n<plist version="1.0"><array><dict><key>choiceAttribute</key><string>customLocation</string><key>attributeSetting</key><string>%s</string><key>choiceIdentifier</key><string>default</string></dict></array></plist>\\n' "$HOME/.todoforai/tools" > /tmp/aws-choices.xml && installer -pkg /tmp/AWSCLIV2.pkg -target CurrentUserHomeDirectory -applyChoiceChangesXML /tmp/aws-choices.xml && mkdir -p "$HOME/.todoforai/tools/bin" && ln -sf "$HOME/.todoforai/tools/aws-cli/aws" "$HOME/.todoforai/tools/bin/aws" && ln -sf "$HOME/.todoforai/tools/aws-cli/aws_completer" "$HOME/.todoforai/tools/bin/aws_completer"; rc=$?; rm -f /tmp/AWSCLIV2.pkg /tmp/aws-choices.xml; exit $rc; else ARCH=$(uname -m); case "$ARCH" in arm64|aarch64) ARCH=aarch64;; *) ARCH=x86_64;; esac; curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$ARCH.zip" -o /tmp/awscli.zip && rm -rf /tmp/aws && unzip -qo /tmp/awscli.zip -d /tmp && /tmp/aws/install --update --install-dir "$HOME/.todoforai/tools/aws-cli" --bin-dir "$HOME/.todoforai/tools/bin"; rc=$?; rm -rf /tmp/aws /tmp/awscli.zip; exit $rc; fi`,
44977
+ statusCmd: "aws sts get-caller-identity --query Arn --output text 2>/dev/null | sed 's/.*\\///' | grep -m1 .",
44978
+ loginCmd: "aws configure set aws_access_key_id <access_key_id> && aws configure set aws_secret_access_key <secret_access_key> && aws configure set region <region>",
44979
+ credentialPaths: ["~/.aws/credentials", "~/.aws/config", "~/.aws/sso/cache"],
44980
+ description: "Official AWS CLI v2, installed user-locally into ~/.todoforai/tools (no sudo; Linux x86_64/aarch64 + macOS). Auth via `aws configure` (access keys) or `aws configure sso` + `aws sso login` (browser device flow).",
44981
+ versionCmd: "aws --version 2>/dev/null | sed -n 1p",
44982
+ uninstallCmd: 'rm -rf "$HOME/.todoforai/tools/aws-cli" "$HOME/.todoforai/tools/bin/aws" "$HOME/.todoforai/tools/bin/aws_completer"; hash -r 2>/dev/null; ! command -v aws >/dev/null 2>&1 || { echo "aws is still on PATH: $(command -v aws) — not installed by TODOforAI, remove it with the package manager that installed it (snap/apt/brew/sudo)"; exit 1; }'
44983
+ },
44984
+ gcloud: {
44985
+ category: "cloud",
44986
+ pkg: "google-cloud-sdk",
44987
+ installer: "system",
44988
+ label: "Google Cloud",
44989
+ summary: "Manage your Google Cloud projects and services.",
44990
+ installCmd: 'curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts && mkdir -p "$HOME/.todoforai/tools/bin" && ln -sf "$HOME/google-cloud-sdk/bin/gcloud" "$HOME/.todoforai/tools/bin/gcloud"',
44991
+ statusCmd: `gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null | tr -d '\\r' | grep -m1 .`,
44992
+ loginCmd: "gcloud auth login --no-launch-browser",
44993
+ loginNeedsPty: true,
44994
+ stdinPaste: true,
44995
+ oauthUrlPattern: "(https://accounts\\.google\\.com/o/oauth2/auth\\S+)",
44996
+ credentialPaths: ["~/.config/gcloud"],
44997
+ description: "Official Google Cloud CLI. `gcloud auth login --no-launch-browser` prints the OAuth URL and waits for the pasted verification code.",
44998
+ versionCmd: "gcloud --version 2>/dev/null | sed -n 1p",
44999
+ uninstallCmd: 'rm -rf "$HOME/google-cloud-sdk" "$HOME/.todoforai/tools/bin/gcloud"; hash -r 2>/dev/null; ! command -v gcloud >/dev/null 2>&1 || { echo "gcloud is still on PATH: $(command -v gcloud)"; exit 1; }',
45000
+ updateCmd: "gcloud components update --quiet"
45001
+ },
45002
+ az: {
45003
+ category: "cloud",
45004
+ pkg: "azure-cli",
45005
+ installer: "pip",
45006
+ label: "Azure",
45007
+ summary: "Manage your Microsoft Azure resources.",
45008
+ binName: "az",
45009
+ statusCmd: "az account show --query user.name -o tsv 2>/dev/null | tr -d '\\r' | grep -m1 .",
45010
+ loginCmd: "az login --use-device-code",
45011
+ oauthUrlPattern: "(https://login\\.microsoft\\.com/device\\S*)",
45012
+ deviceCodePattern: "enter the code ([A-Z0-9]+)",
45013
+ canStreamLogin: true,
45014
+ credentialPaths: ["~/.azure"],
45015
+ description: "Official Azure CLI. Device-code login: prints login.microsoft.com/device + a code, then polls — no callback port.",
45016
+ versionCmd: `az version --query '"azure-cli"' -o tsv 2>/dev/null | sed -n 1p`
45017
+ },
45018
+ doctl: {
45019
+ preinstallCloud: true,
45020
+ cloudInstallCmd: "curl -fsSL https://github.com/digitalocean/doctl/releases/download/v1.168.0/doctl-1.168.0-linux-amd64.tar.gz | tar -xz -C /usr/local/bin doctl",
45021
+ cloudVerifyCmd: "doctl version",
45022
+ category: "cloud",
45023
+ pkg: "doctl",
45024
+ installer: "binary",
45025
+ label: "DigitalOcean",
45026
+ summary: "Manage your DigitalOcean servers and apps.",
45027
+ statusCmd: "doctl account get --format Email --no-header 2>/dev/null | tr -d '\\r' | grep -m1 .",
45028
+ loginCmd: "doctl auth init -t <access_token>",
45029
+ loginNeedsPty: true,
45030
+ credentialPaths: ["~/.config/doctl/config.yaml"],
45031
+ description: "Official DigitalOcean CLI. `doctl auth init` prompts for an API token from cloud.digitalocean.com/account/api/tokens.",
45032
+ versionCmd: "doctl version 2>/dev/null | sed -n 1p"
45033
+ },
45034
+ rdt: {
45035
+ preinstallCloud: true,
45036
+ packages: ["rdt-cli==0.4.1"],
45037
+ category: "social",
45038
+ pkg: "rdt-cli",
45039
+ installer: "pip",
45040
+ label: "Reddit",
45041
+ summary: "Search Reddit, read threads and post comments.",
45042
+ statusCmd: `rdt status 2>&1 | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45043
+ loginCmd: "rdt login",
45044
+ loginTerminalHint: "Log in to reddit.com in a browser on this host (Chrome/Firefox), then run `rdt login`: it imports that browser's Reddit cookies.",
45045
+ credentialPaths: ["~/.config/rdt-cli/credential.json"],
45046
+ description: 'Reddit CLI. Read: `rdt search "<query>" -r <sub> --json -c`, `rdt sub <name>`, `rdt read <postId>`, `rdt user-comments <user>`. Write: `rdt comment <postId> "<text>"`, `rdt upvote`, `rdt save`.',
45047
+ versionCmd: "rdt --version 2>/dev/null | sed -n 1p"
45048
+ },
45049
+ pymupdf: {
45050
+ category: "utility",
45051
+ pkg: "pymupdf",
45052
+ packages: ["pymupdf==1.28.2"],
45053
+ installer: "pip",
45054
+ label: "PDF",
45055
+ summary: "Read, edit, merge and split PDF files.",
45056
+ versionCmd: "{ python -c 'import pymupdf; print(pymupdf.__version__)' || python3 -c 'import pymupdf; print(pymupdf.__version__)' || py -c 'import pymupdf; print(pymupdf.__version__)'; } 2>/dev/null",
45057
+ preinstallCloud: true,
45058
+ cloudVerifyCmd: "python3 -c 'import pymupdf'",
45059
+ preinstallJarvis: true,
45060
+ description: "PDF text editing, extraction, merge, split. Call via `python3 -c`."
45061
+ },
45062
+ pyhanko: {
45063
+ category: "security",
45064
+ pkg: "pyhanko-cli",
45065
+ packages: ["pyhanko-cli==0.5.0", "pyHanko[pkcs11,image-support,opentype]==0.37.0"],
45066
+ installer: "pip",
45067
+ binName: "pyhanko",
45068
+ label: "PDF Signing",
45069
+ summary: "Digitally sign and verify PDFs with a certificate (PAdES, timestamps, smart cards).",
45070
+ versionCmd: "pyhanko --version 2>/dev/null | sed -n 1p",
45071
+ preinstallCloud: true,
45072
+ cloudVerifyCmd: "pyhanko --version",
45073
+ preinstallJarvis: true,
45074
+ description: "Cryptographic PDF signing/validation (PAdES B-B..B-LTA). Sign: `pyhanko sign addsig --field Sig1 [--timestamp-url <tsa>] pkcs12 --passfile pass.txt in.pdf out.pdf cert.p12`; smart card/eID: `... pkcs11 --lib <pkcs11.so> in.pdf out.pdf`; verify: `pyhanko sign validate --pretty-print [--trust ca.pem] out.pdf`; list fields: `pyhanko sign list`. The signer's cert/key comes from the user (vault); self-signed certs validate only with `--trust`. Not for click-to-sign workflows."
45075
+ },
45076
+ anydoc: {
45077
+ category: "utility",
45078
+ pkg: "@firecrawl/anydoc",
45079
+ installer: "npm",
45080
+ binName: "anydoc",
45081
+ label: "Doc to Markdown",
45082
+ summary: "Turn Word, PowerPoint, Excel and other documents into clean text.",
45083
+ description: "Office/rich docs (docx/pptx/xlsx/odt/rtf/epub/csv) → Markdown, fully local. `anydoc file.docx` → stdout, `-o out.md` writes, `anydoc - --format csv < stdin`. Prefer over raw XML extraction for reading; one-way, no OCR for scanned PDFs.",
45084
+ versionCmd: "anydoc --version 2>/dev/null | sed -n 1p",
45085
+ preinstallCloud: true,
45086
+ preinstallJarvis: true
45087
+ },
45088
+ "agent-browser": {
45089
+ category: "development",
45090
+ pkg: "agent-browser@0.30.1",
45091
+ installer: "npm",
45092
+ preinstallCloud: true,
45093
+ label: "Browser",
45094
+ summary: "Browse websites automatically: click, type, read and screenshot.",
45095
+ description: "Browser automation over CDP (`open`/`snapshot`/`click`/`type`/`fill`/`read`/`screenshot`/`eval`). `connect '<url>'` to a listed live browser (prefer the user's real Chrome), verify with `snapshot`; if empty, `close` (own session only) and reconnect once. NEVER `close --all`. Shared browser: `tab new` for your own tab, `tab <id>` before each command. No URL listed: cloud → browser-manager-cli; PC with display → Chrome `--remote-debugging-port=9222`, attach `--cdp 9222`.",
45096
+ versionCmd: "agent-browser --version 2>/dev/null | sed -n 1p",
45097
+ preinstallJarvis: true
45098
+ },
45099
+ "browser-manager-cli": {
45100
+ category: "development",
45101
+ pkg: "browser-manager-cli",
45102
+ installer: "binary",
45103
+ binName: "browser-manager-cli",
45104
+ preinstallCloud: true,
45105
+ label: "Browser Manager",
45106
+ summary: "Start browsers in the cloud for automated browsing.",
45107
+ platformAuth: true,
45108
+ description: "Spawns cloud Chromium sessions, each exposed as a CDP WebSocket for agent-browser to drive. `create` prints one line ending in a cdpUrl — pass it straight to `agent-browser connect '<cdpUrl>'` (connect the cdpUrl, not the Chrome debug port). `list`/`get <id>`, `delete <id>`/`delete-all`, `hibernate`/`restore`. Sessions run stealth Chromium that reads as a real desktop browser. For sites that block datacenter IPs, route through a proxy: `create --proxy socks5://host:port [--proxy-user U --proxy-pass P]`. Use this on the cloud VM instead of launching a local browser.",
45109
+ versionCmd: "browser-manager-cli version 2>/dev/null | sed -n 1p",
45110
+ binary: {
45111
+ "linux-x86_64": {
45112
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-linux-x86_64",
45113
+ archive: "raw"
45114
+ },
45115
+ "linux-aarch64": {
45116
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-linux-aarch64",
45117
+ archive: "raw"
45118
+ },
45119
+ "darwin-x86_64": {
45120
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-darwin-x86_64",
45121
+ archive: "raw"
45122
+ },
45123
+ "darwin-aarch64": {
45124
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-darwin-aarch64",
45125
+ archive: "raw"
45126
+ },
45127
+ "windows-x86_64": {
45128
+ url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-windows-x86_64.exe",
45129
+ archive: "raw"
45130
+ }
45131
+ },
45132
+ preinstallJarvis: true
45133
+ },
45134
+ "todoforai-browser": {
45135
+ category: "development",
45136
+ pkg: "@todoforai/browser",
45137
+ aliases: ["tfa-browser"],
45138
+ installer: "npm",
45139
+ preinstallCloud: true,
45140
+ label: "Browser (Extension)",
45141
+ summary: "Drive your own browser through the TODOforAI extension.",
45142
+ versionCmd: `node -p "require(require('fs').realpathSync(require('child_process').execSync('command -v todoforai-browser',{encoding:'utf8'}).trim()).replace(/\\/dist\\/index\\.js$/, '/package.json')).version" 2>/dev/null`,
45143
+ internal: true,
45144
+ preinstallJarvis: true,
45145
+ description: "Browser automation via extension (non-headless), web scraping, accessibility tree snapshots, screenshots, PDF generation"
45146
+ },
45147
+ "tfa-explore": {
45148
+ preinstallCloud: true,
45149
+ category: "development",
45150
+ pkg: "@todoforai/tfa-explore",
45151
+ installer: "npm",
45152
+ label: "Explore",
45153
+ summary: "Send a helper agent to explore a codebase and report back.",
45154
+ platformAuth: true,
45155
+ description: 'Sub-agent codebase exploration in its own context, you get only the answer: `tfa-explore "where is auth handled?" [--repo <path>]`. Prefer over manual multi-file searching.',
45156
+ versionCmd: "tfa-explore --version 2>/dev/null | sed -n 1p",
45157
+ installCmd: "npm install -g @todoforai/tfa-explore",
45158
+ internal: true
45159
+ },
45160
+ "tfa-review": {
45161
+ preinstallCloud: true,
45162
+ category: "development",
45163
+ pkg: "@todoforai/tfa-review",
45164
+ installer: "npm",
45165
+ label: "Review",
45166
+ summary: "Send a helper agent to review your code changes.",
45167
+ platformAuth: true,
45168
+ description: 'Sub-agent review of a git diff in its own context: `tfa-review "<goal>" [--repo <path>] [--against <ref>]` (default: uncommitted). Run before calling a non-trivial change done.',
45169
+ versionCmd: "tfa-review --version 2>/dev/null | sed -n 1p",
45170
+ installCmd: "npm install -g @todoforai/tfa-review",
45171
+ internal: true
45172
+ },
45173
+ "tfa-summary": {
45174
+ preinstallCloud: true,
45175
+ category: "development",
45176
+ pkg: "@todoforai/tfa-summary",
45177
+ installer: "npm",
45178
+ label: "Summary",
45179
+ summary: "Summarize files or long output with a helper agent.",
45180
+ platformAuth: true,
45181
+ description: 'Sub-agent summary so raw text never enters your context: `cat big.log | tfa-summary "what failed?"`, or `-f <file>` (repeatable).',
45182
+ versionCmd: "tfa-summary --version 2>/dev/null | sed -n 1p",
45183
+ installCmd: "npm install -g @todoforai/tfa-summary",
45184
+ internal: true
45185
+ },
45186
+ "tfa-handoff": {
45187
+ preinstallCloud: true,
45188
+ category: "development",
45189
+ pkg: "@todoforai/tfa-handoff",
45190
+ installer: "npm",
45191
+ label: "Handoff",
45192
+ summary: "Turn a long conversation into a short handover note.",
45193
+ platformAuth: true,
45194
+ description: "Compact a conversation into a handoff document for a fresh agent.",
45195
+ versionCmd: "tfa-handoff --version 2>/dev/null | sed -n 1p",
45196
+ installCmd: "npm install -g @todoforai/tfa-handoff",
45197
+ internal: true
45198
+ },
45199
+ "tfa-wait": {
45200
+ category: "development",
45201
+ pkg: "@todoforai/tfa-wait",
45202
+ installer: "npm",
45203
+ label: "Wait",
45204
+ summary: "Wait for a time or an event, then continue the task.",
45205
+ platformAuth: true,
45206
+ description: "Schedule a wake-up: `tfa-wait '<expr>' \"<message>\"` — expr is a time (`3h`, `daily@9:00`), a webhook, or a command's output; the message arrives later as a new chat message. Wait for a long job: `tfa-wait 'bench || 6h' \"Done: exit {{payload.exit}}\"` then `tfa-wait run '<cmd>' --pipe bench --on-exit &` (each stdout line = event; cancelling kills the command). Never pgrep/sleep poll loops.",
45207
+ versionCmd: "tfa-wait --version 2>/dev/null | sed -n 1p",
45208
+ installCmd: "npm install -g @todoforai/tfa-wait",
45209
+ preinstallCloud: true,
45210
+ internal: true,
45211
+ preinstallJarvis: true
45212
+ },
45213
+ "tfa-vault": {
45214
+ category: "security",
45215
+ pkg: "@todoforai/tfa-vault",
45216
+ installer: "npm",
45217
+ label: "Vault",
45218
+ summary: "Your password and secret store.",
45219
+ platformAuth: true,
45220
+ description: "The user's encrypted secret vault — using its secrets is the intended workflow. READ: when a task needs a key/token/password the user owns, `tfa-vault list` then `get <path>`; don't ask for what's already stored; never echo values. WRITE proactively: any new secret in the conversation (pasted, generated, from a login) → `put <path> field=value` (`patch` adds fields; `put` refuses an existing path — `put --force` replaces all fields on rotation). Paths: `accounts/<svc>`, `api/<svc>`, `secrets/<name>`.",
45221
+ versionCmd: "tfa-vault --version 2>/dev/null | sed -n 1p",
45222
+ installCmd: "npm install -g @todoforai/tfa-vault",
45223
+ preinstallCloud: true,
45224
+ preinstallJarvis: true
45225
+ },
45226
+ "tfa-settings": {
45227
+ category: "utility",
45228
+ pkg: "@todoforai/tfa-settings",
45229
+ installer: "npm",
45230
+ label: "Settings",
45231
+ summary: "Change your TODOforAI app preferences from the shell.",
45232
+ platformAuth: true,
45233
+ description: "The user's TODOforAI app preferences (AppSettings): `list`, `get <key>`, `set key=value …`, `keys` (documented keys). Use when asked to change theme, list style, tool-block density, thinking traces, sounds/toasts, preview columns, wallpaper etc. Values are JSON when parseable. Consent/credential/escalation keys (shellExecution, notifications, analytics, keyboard bindings, vault provider, billing address) are refused from an agent shell — tell the user to change those in Settings.",
45234
+ versionCmd: "tfa-settings --version 2>/dev/null | sed -n 1p",
45235
+ installCmd: "npm install -g @todoforai/tfa-settings",
45236
+ preinstallCloud: true,
45237
+ preinstallJarvis: true
45238
+ },
45239
+ "tfa-memory": {
45240
+ category: "productivity",
45241
+ pkg: "@todoforai/tfa-memory",
45242
+ installer: "npm",
45243
+ label: "Memory",
45244
+ summary: "Your long-term memory of notes, facts and past TODOs.",
45245
+ platformAuth: true,
45246
+ description: 'User\'s long-term memory (notes/procedures/decisions, address `source/ref`) plus every past TODO message (read-only, `todo/<todoId>:<messageId>`). READ: `search "<topic>" --mode fused` first (`lexical` = all words newest first, `semantic` = meaning; `--source todo` / `--todo <id>` narrow). stderr prints `N row(s) contain a query word` — if N exceeds what you saw, page with `--exclude <address>`. `get <address>` = full row, `around <address> --before N --after N` = surrounding dialogue. Before answering from memory, search once more with your draft\'s key facts. WRITE durable facts proactively: `add <source>/<ref> "text"` (same address updates in place; short source names like deploy-notes, preferences). `sources` lists them.',
45247
+ versionCmd: "tfa-memory --version 2>/dev/null | sed -n 1p",
45248
+ installCmd: "npm install -g @todoforai/tfa-memory",
45249
+ preinstallCloud: true,
45250
+ preinstallJarvis: true
45251
+ },
45252
+ "todoregistry-cli": {
45253
+ category: "development",
45254
+ pkg: "@todoforai/todoregistry-cli",
45255
+ installer: "npm",
45256
+ label: "TODO Registry",
45257
+ summary: "Browse ready-made business task templates.",
45258
+ description: "Reusable business task templates (SEO, email, CRM, ads, social, …): `search <q>`, `get <id>`, `list [category]`, `categories`, `create --name … --description … --body @prompt.md`. To recommend a template, don't create a todo directly — `tfa-cli recommend --template <id> --project <projectId>`.",
45259
+ versionCmd: "todoregistry-cli --version 2>/dev/null | sed -n 1p",
45260
+ installCmd: "npm install -g @todoforai/todoregistry-cli",
45261
+ preinstallCloud: true,
45262
+ internal: true,
45263
+ preinstallJarvis: true
45264
+ },
45265
+ "tfa-surface": {
45266
+ preinstallCloud: true,
45267
+ category: "utility",
45268
+ pkg: "@todoforai/tfa-surface",
45269
+ installer: "npm",
45270
+ label: "Surface",
45271
+ summary: "See and control what is on your TODOforAI screen.",
45272
+ platformAuth: true,
45273
+ description: "The user's live UI from the shell. Everything on screen is a SURFACE holding one ref (a todo id, `<todoId>:<alias>` = a shown artifact, or a url). `describe` lists them (kind, id, ref, focus, embedded-browser CDP URL; `--json`). `open overlay <todoId>` (full page), `open window <ref>` (floating), `open panel changes|preview|browser|terminal [--url]` (a todo's side panel), `open card <ref> [--title] [--group <slug>]` (a persisted board card every member sees); `close|focus <kind> <id>`. Persistent state (plans, reports, live status) → `show chat.panel <todoId> \"...md\"` (stdin ok; re-show updates in place), `clear` removes; events go in chat. `card add|list|get|rm <name>` = project HTML renderers. Exit 2 = no frontend connected.",
45274
+ versionCmd: "tfa-surface --version 2>/dev/null | sed -n 1p",
45275
+ installCmd: "npm install -g @todoforai/tfa-surface",
45276
+ internal: true
45277
+ },
45278
+ bun: {
45279
+ commodity: true,
45280
+ category: "development",
45281
+ pkg: "bun",
45282
+ installer: "system",
45283
+ label: "JS/TS Runtime",
45284
+ summary: "Run JavaScript and TypeScript code, fast.",
45285
+ versionCmd: "bun --version 2>/dev/null",
45286
+ preinstallCloud: true,
45287
+ cloudInstallCmd: "export BUN_INSTALL=/usr/local && curl -fsSL https://bun.sh/install | bash && ln -sf /usr/local/bin/bun /usr/local/bin/bunx",
45288
+ cloudVerifyCmd: "bun --version",
45289
+ internal: true,
45290
+ preinstallJarvis: true,
45291
+ description: "Run JavaScript/TypeScript, install packages, run tests (`bun`, `bunx`)."
45292
+ },
45293
+ bunx: {
45294
+ commodity: true,
45295
+ category: "development",
45296
+ pkg: "bunx",
45297
+ installer: "system",
45298
+ label: "Bun Package Runner",
45299
+ summary: "Run a JavaScript package without installing it.",
45300
+ versionCmd: "bunx --version 2>/dev/null",
45301
+ preinstallCloud: true,
45302
+ cloudVerifyCmd: "bunx --version",
45303
+ internal: true,
45304
+ description: "Run package binaries with Bun."
45305
+ },
45306
+ node: {
45307
+ commodity: true,
45308
+ category: "development",
45309
+ pkg: "node",
45310
+ installer: "system",
45311
+ label: "JavaScript Runtime",
45312
+ summary: "Run JavaScript code.",
45313
+ versionCmd: "node -p 'process.version' 2>/dev/null",
45314
+ preinstallCloud: true,
45315
+ cloudInstallCmd: "curl -fsSL https://deb.nodesource.com/setup_24.x | bash -",
45316
+ cloudAptPackages: ["nodejs"],
45317
+ cloudVerifyCmd: "node --version",
45318
+ internal: true,
45319
+ preinstallJarvis: true,
45320
+ description: "Run Node.js scripts."
45321
+ },
45322
+ python3: {
45323
+ commodity: true,
45324
+ category: "development",
45325
+ pkg: "python3",
45326
+ installer: "system",
45327
+ label: "Python",
45328
+ summary: "Run Python code.",
45329
+ versionCmd: "{ python3 --version || python --version || py --version; } 2>/dev/null | sed -n 1p",
45330
+ preinstallCloud: true,
45331
+ cloudAptPackages: ["python3"],
45332
+ cloudVerifyCmd: "python3 --version",
45333
+ internal: true,
45334
+ preinstallJarvis: true,
45335
+ description: "Run Python scripts."
45336
+ },
45337
+ npm: {
45338
+ commodity: true,
45339
+ category: "development",
45340
+ pkg: "npm",
45341
+ installer: "system",
45342
+ label: "Node Package Manager",
45343
+ summary: "Install JavaScript packages.",
45344
+ versionCmd: "npm --version 2>/dev/null",
45345
+ preinstallCloud: true,
45346
+ cloudVerifyCmd: "npm --version",
45347
+ internal: true,
45348
+ description: "Install Node packages and run package scripts."
45349
+ },
45350
+ npx: {
45351
+ commodity: true,
45352
+ category: "development",
45353
+ pkg: "npx",
45354
+ installer: "system",
45355
+ label: "Node Package Runner",
45356
+ summary: "Run a JavaScript package without installing it.",
45357
+ versionCmd: "npx --version 2>/dev/null",
45358
+ preinstallCloud: true,
45359
+ cloudVerifyCmd: "npx --version",
45360
+ internal: true,
45361
+ description: "Run npm package binaries."
45362
+ },
45363
+ pip3: {
45364
+ preinstallCloud: true,
45365
+ cloudAptPackages: ["python3-pip"],
45366
+ cloudVerifyCmd: "pip3 --version",
45367
+ commodity: true,
45368
+ category: "development",
45369
+ pkg: "pip3",
45370
+ installer: "system",
45371
+ label: "Python Package Installer",
45372
+ summary: "Install Python packages.",
45373
+ versionCmd: "pip3 --version 2>/dev/null | sed -n 1p | cut -d' ' -f1-2",
45374
+ internal: true,
45375
+ description: "Install Python packages."
45376
+ },
45377
+ uv: {
45378
+ commodity: true,
45379
+ category: "development",
45380
+ pkg: "uv",
45381
+ installer: "system",
45382
+ label: "Python Package Manager",
45383
+ summary: "Install Python packages and manage environments, fast.",
45384
+ versionCmd: "uv --version 2>/dev/null",
45385
+ preinstallCloud: true,
45386
+ cloudInstallCmd: "curl -LsSf https://astral.sh/uv/0.9.6/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh",
45387
+ cloudVerifyCmd: "uv --version",
45388
+ internal: true,
45389
+ description: "Manage Python dependencies, environments, tools, and scripts (`uv`, `uvx`)."
45390
+ },
45391
+ uvx: {
45392
+ commodity: true,
45393
+ category: "development",
45394
+ pkg: "uvx",
45395
+ installer: "system",
45396
+ label: "Python Tool Runner",
45397
+ summary: "Run a Python tool without installing it.",
45398
+ versionCmd: "uvx --version 2>/dev/null",
45399
+ preinstallCloud: true,
45400
+ cloudVerifyCmd: "uvx --version",
45401
+ internal: true,
45402
+ description: "Run Python tools in isolated environments."
45403
+ },
45404
+ ripgrep: {
45405
+ commodity: true,
45406
+ category: "development",
45407
+ pkg: "ripgrep",
45408
+ installer: "binary",
45409
+ label: "Search",
45410
+ summary: "Search text across files, very fast.",
45411
+ description: "Fast recursive grep with regex, .gitignore-aware, parallel, unicode-correct",
45412
+ versionCmd: "rg --version 2>/dev/null | sed -n 1p",
45413
+ preinstallCloud: true,
45414
+ binary: {
45415
+ "linux-x86_64": {
45416
+ url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz",
45417
+ archive: "tar.gz",
45418
+ extract: "ripgrep-14.1.1-x86_64-unknown-linux-musl/rg"
45419
+ },
45420
+ "linux-aarch64": {
45421
+ url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz",
45422
+ archive: "tar.gz",
45423
+ extract: "ripgrep-14.1.1-aarch64-unknown-linux-gnu/rg"
45424
+ },
45425
+ "darwin-x86_64": {
45426
+ url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-apple-darwin.tar.gz",
45427
+ archive: "tar.gz",
45428
+ extract: "ripgrep-14.1.1-x86_64-apple-darwin/rg"
45429
+ },
45430
+ "darwin-aarch64": {
45431
+ url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz",
45432
+ archive: "tar.gz",
45433
+ extract: "ripgrep-14.1.1-aarch64-apple-darwin/rg"
45434
+ },
45435
+ "windows-x86_64": {
45436
+ url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-pc-windows-msvc.zip",
45437
+ archive: "zip",
45438
+ extract: "ripgrep-14.1.1-x86_64-pc-windows-msvc/rg.exe"
45439
+ }
45440
+ },
45441
+ binName: "rg",
45442
+ internal: true,
45443
+ preinstallJarvis: true
45444
+ },
45445
+ "ast-grep": {
45446
+ category: "development",
45447
+ pkg: "ast-grep",
45448
+ installer: "binary",
45449
+ label: "Structural Search",
45450
+ summary: "Search and rewrite code by its structure, not just text.",
45451
+ description: 'Use when regex cannot distinguish syntax from text: `ast-grep -p "foo($$$)" -l ts` finds real calls while ignoring strings and comments; add `--rewrite "..."` for codemods. Prefer `rg` for literal or token search.',
45452
+ versionCmd: "ast-grep --version 2>/dev/null | sed -n 1p",
45453
+ preinstallCloud: true,
45454
+ binary: {
45455
+ "linux-x86_64": {
45456
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-unknown-linux-gnu.zip",
45457
+ archive: "zip",
45458
+ extract: "ast-grep"
45459
+ },
45460
+ "linux-aarch64": {
45461
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-aarch64-unknown-linux-gnu.zip",
45462
+ archive: "zip",
45463
+ extract: "ast-grep"
45464
+ },
45465
+ "darwin-x86_64": {
45466
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-apple-darwin.zip",
45467
+ archive: "zip",
45468
+ extract: "ast-grep"
45469
+ },
45470
+ "darwin-aarch64": {
45471
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-aarch64-apple-darwin.zip",
45472
+ archive: "zip",
45473
+ extract: "ast-grep"
45474
+ },
45475
+ "windows-x86_64": {
45476
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-pc-windows-msvc.zip",
45477
+ archive: "zip",
45478
+ extract: "ast-grep.exe"
45479
+ }
45480
+ },
45481
+ binName: "ast-grep",
45482
+ internal: true,
45483
+ preinstallJarvis: true
45484
+ },
45485
+ fd: {
45486
+ commodity: true,
45487
+ category: "development",
45488
+ pkg: "fd-find",
45489
+ installer: "binary",
45490
+ label: "Find Files",
45491
+ summary: "Find files and folders by name.",
45492
+ description: "Fast user-friendly find replacement: regex by default, .gitignore-aware, parallel",
45493
+ versionCmd: "fd --version 2>/dev/null | sed -n 1p",
45494
+ preinstallCloud: true,
45495
+ binary: {
45496
+ "linux-x86_64": {
45497
+ url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-unknown-linux-musl.tar.gz",
45498
+ archive: "tar.gz",
45499
+ extract: "fd-v10.2.0-x86_64-unknown-linux-musl/fd"
45500
+ },
45501
+ "linux-aarch64": {
45502
+ url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-aarch64-unknown-linux-gnu.tar.gz",
45503
+ archive: "tar.gz",
45504
+ extract: "fd-v10.2.0-aarch64-unknown-linux-gnu/fd"
45505
+ },
45506
+ "darwin-x86_64": {
45507
+ url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-apple-darwin.tar.gz",
45508
+ archive: "tar.gz",
45509
+ extract: "fd-v10.2.0-x86_64-apple-darwin/fd"
45510
+ },
45511
+ "darwin-aarch64": {
45512
+ url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-aarch64-apple-darwin.tar.gz",
45513
+ archive: "tar.gz",
45514
+ extract: "fd-v10.2.0-aarch64-apple-darwin/fd"
45515
+ },
45516
+ "windows-x86_64": {
45517
+ url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-pc-windows-msvc.zip",
45518
+ archive: "zip",
45519
+ extract: "fd-v10.2.0-x86_64-pc-windows-msvc/fd.exe"
45520
+ }
45521
+ },
45522
+ binName: "fd",
45523
+ internal: true,
45524
+ preinstallJarvis: true
45525
+ },
45526
+ jq: {
45527
+ commodity: true,
45528
+ category: "development",
45529
+ pkg: "jq",
45530
+ installer: "binary",
45531
+ label: "JSON",
45532
+ summary: "Read and reshape JSON data.",
45533
+ description: "Command-line JSON processor: query, filter, transform, format JSON streams",
45534
+ versionCmd: "jq --version 2>/dev/null | sed -n 1p",
45535
+ preinstallCloud: true,
45536
+ binary: {
45537
+ "linux-x86_64": {
45538
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64",
45539
+ archive: "raw"
45540
+ },
45541
+ "linux-aarch64": {
45542
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-arm64",
45543
+ archive: "raw"
45544
+ },
45545
+ "darwin-x86_64": {
45546
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-amd64",
45547
+ archive: "raw"
45548
+ },
45549
+ "darwin-aarch64": {
45550
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64",
45551
+ archive: "raw"
45552
+ },
45553
+ "windows-x86_64": {
45554
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-windows-amd64.exe",
45555
+ archive: "raw"
45556
+ }
45557
+ },
45558
+ binName: "jq",
45559
+ internal: true,
45560
+ preinstallJarvis: true
45561
+ },
45562
+ patch: {
45563
+ commodity: true,
45564
+ category: "development",
45565
+ pkg: "patch",
45566
+ installer: "system",
45567
+ preinstallCloud: true,
45568
+ label: "Apply Diff",
45569
+ summary: "Apply a diff to files.",
45570
+ description: "Apply unified/context diff patches to files; reverse, dry-run, fuzzy matching",
45571
+ versionCmd: "patch --version 2>/dev/null | sed -n 1p",
45572
+ binName: "patch",
45573
+ internal: true,
45574
+ preinstallJarvis: true
45575
+ },
45576
+ sed: {
45577
+ commodity: true,
45578
+ category: "development",
45579
+ pkg: "sed",
45580
+ installer: "system",
45581
+ preinstallCloud: true,
45582
+ label: "Replace Text",
45583
+ summary: "Find and replace text in files.",
45584
+ description: "Stream editor: in-place text substitution, line filtering, scripted edits",
45585
+ versionCmd: "sed --version 2>/dev/null | sed -n 1p",
45586
+ binName: "sed",
45587
+ internal: true,
45588
+ preinstallJarvis: true
45589
+ },
45590
+ grep: {
45591
+ commodity: true,
45592
+ category: "development",
45593
+ pkg: "grep",
45594
+ installer: "system",
45595
+ preinstallCloud: true,
45596
+ label: "Search Text",
45597
+ summary: "Search for text in files.",
45598
+ description: "Search plain text for lines matching a regex: recursive, fixed-string, context, invert, count",
45599
+ versionCmd: "grep --version 2>/dev/null | sed -n 1p",
45600
+ binName: "grep",
45601
+ internal: true,
45602
+ preinstallJarvis: true
45603
+ },
45604
+ "wordpress-api": {
45605
+ preinstallCloud: true,
45606
+ category: "development",
45607
+ pkg: "@todoforai/wordpress-api",
45608
+ installer: "npm",
45609
+ label: "WordPress",
45610
+ summary: "Read and publish WordPress posts and pages.",
45611
+ statusCmd: `wordpress-api whoami 2>/dev/null | sed -n 's/.*"slug"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45612
+ loginCmd: "wordpress-api auth --site <url> --user <username> --password <app_password>",
45613
+ credentialPaths: ["~/.config/wordpress-api/credentials.json"],
45614
+ description: "WordPress REST API CLI. `wordpress-api auth --site https://example.com --user <user> --password '<application password>'` (application password, not the login password). Reading published content needs no auth. `posts list|get|create|update|delete`, `pages`, `media`, `categories`. `posts create` saves a draft unless --status publish. On Elementor/JetEngine sites content.rendered comes back empty — fetch the post's public URL for the body.",
45615
+ versionCmd: "wordpress-api --version 2>/dev/null | sed -n 1p"
45616
+ },
45617
+ "facebook-pages-api": {
45618
+ preinstallCloud: true,
45619
+ category: "social",
45620
+ pkg: "@todoforai/facebook-pages-api",
45621
+ installer: "npm",
45622
+ label: "Facebook Pages",
45623
+ summary: "Post to your Facebook Page and handle comments.",
45624
+ statusCmd: `facebook-pages-api whoami 2>/dev/null | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45625
+ loginCmd: "facebook-pages-api auth --page-id <id> --page-token <token>",
45626
+ credentialPaths: ["~/.config/facebook-pages-api/credentials.json"],
45627
+ description: "Facebook Pages CLI (Meta Graph API). `auth --page-id <id> --page-token <token>` (or env FB_PAGE_ID/FB_PAGE_TOKEN). `feed`, `comments`, `insights`, `conversations`, `media`, and `request <GET|POST|DELETE> <path> [--data <json>]` = any Graph edge with the page token attached (use instead of curl). Writes are live: `feed create --published false` for tests; `comments reply` posts publicly — get human approval. Follower totals: `whoami --fields followers_count,fan_count` (many classic insights metrics are retired). `--help` for subcommands.",
45628
+ versionCmd: "facebook-pages-api --version 2>/dev/null | sed -n 1p"
45629
+ },
45630
+ "ews-api": {
45631
+ category: "email",
45632
+ pkg: "ews-api",
45633
+ installer: "pip",
45634
+ label: "Exchange (EWS)",
45635
+ summary: "Read and write mail on a company Exchange server.",
45636
+ statusCmd: `ews-api whoami 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45637
+ loginCmd: "ews-api auth --email <email> --username <username> --server <server> --password <password>",
45638
+ credentialPaths: ["~/.config/ews-api/credentials.json"],
45639
+ description: "On-prem Microsoft Exchange mail over EWS (cloud/M365 → outlook-api). `auth --email <e> --username 'DOMAIN\\\\user' --server <host>` (NTLM; password prompt or `--password-stdin`; --server required). Multi-mailbox: `whoami`, `--account <email>`/EWS_ACCOUNT, `default`, `logout`; EWS_EMAIL/EWS_USERNAME/EWS_PASSWORD/EWS_SERVER for unattended runs. `list`, `search <q>`, `read <id>`, `draft`, `reply <id>` — draft/reply stay in Drafts unless --send. Password stored plaintext in ~/.config/ews-api (0600); re-auth after rotation.",
45640
+ versionCmd: "ews-api --version 2>/dev/null | sed -n 1p",
45641
+ logoutCmd: "ews-api logout {{ACCOUNT}}"
45642
+ },
45643
+ "outlook-api": {
45644
+ preinstallCloud: true,
45645
+ category: "email",
45646
+ pkg: "@todoforai/outlook-api",
45647
+ installer: "npm",
45648
+ label: "Outlook (Graph)",
45649
+ summary: "Read and write your Outlook mail.",
45650
+ statusCmd: `outlook-api whoami 2>/dev/null | sed -n 's/.*"userPrincipalName"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45651
+ loginCmd: "outlook-api auth",
45652
+ credentialPaths: ["~/.config/outlook-api/config.json"],
45653
+ description: "Outlook/Hotmail/M365 mail over Microsoft Graph (on-prem Exchange → ews-api). `auth` = device-code flow; `--tenant consumers` (default, personal) | `organizations`. list/search/read/attachments are read-only; `draft`/`reply` stay in Drafts unless --send; `send <draftId>`. `get <path>` = raw Graph GET. JSON out.",
45654
+ versionCmd: "outlook-api --version 2>/dev/null | sed -n 1p",
45655
+ deviceCodePattern: "enter the code ([A-Z0-9]+)",
45656
+ oauthUrlPattern: "(https://(?:www\\.microsoft\\.com/link|login\\.microsoft\\.com/device))",
45657
+ canStreamLogin: true
45658
+ },
45659
+ neonctl: {
45660
+ category: "database",
45661
+ pkg: "neonctl",
45662
+ installer: "npm",
45663
+ label: "Neon Postgres",
45664
+ summary: "Manage your Neon Postgres databases and branches.",
45665
+ statusCmd: `test -f ~/.config/neon/credentials.json && neonctl me --output json 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45666
+ loginCmd: "neonctl auth",
45667
+ oauthUrlPattern: "(https://oauth2\\.neon\\.tech/oauth2/auth\\S+)",
45668
+ credentialPaths: ["~/.config/neon/credentials.json"],
45669
+ description: "Manage Neon Postgres branches and run SQL: `neonctl projects list`, `neonctl branches create`, `neonctl connection-string`. Aliased as `neon`.",
45670
+ versionCmd: "neonctl --version 2>/dev/null | sed -n 1p",
45671
+ aliases: ["neon"],
45672
+ canStreamLogin: true
45673
+ },
45674
+ m365: {
45675
+ preinstallCloud: true,
45676
+ category: "productivity",
45677
+ pkg: "@pnp/cli-microsoft365",
45678
+ installer: "npm",
45679
+ label: "Microsoft 365",
45680
+ summary: "Work with Microsoft 365 mail, Teams, SharePoint and users.",
45681
+ statusCmd: `m365 status --output text 2>/dev/null | tr -d '"' | grep -vi 'logged out' | grep -m1 .`,
45682
+ loginCmd: "m365 login --authType deviceCode --appId 04b07795-8ddb-461a-bbee-02f9e1bf7b46",
45683
+ deviceCodePattern: "enter the code ([A-Z0-9]{8,}) to authenticate",
45684
+ oauthUrlPattern: "open the page (https://\\S+) and enter",
45685
+ credentialPaths: ["~/.cli-m365-msal.json", "~/.cli-m365-connection.json"],
45686
+ description: "Microsoft 365 from the shell: `m365 outlook message list`, `m365 teams team list`, `m365 spo site list`. Device-code login; add `--output json` for parseable output.",
45687
+ versionCmd: `m365 version 2>/dev/null | tr -d '"' | sed -n 1p`
45688
+ },
45689
+ hubspot: {
45690
+ preinstallCloud: true,
45691
+ category: "marketing",
45692
+ pkg: "@hubspot/cli",
45693
+ installer: "npm",
45694
+ label: "HubSpot",
45695
+ summary: "Work with your HubSpot contacts, deals and content.",
45696
+ binName: "hs",
45697
+ statusCmd: "hs account list >/dev/null 2>&1 && echo authenticated",
45698
+ loginCmd: "hs account auth",
45699
+ oauthUrlPattern: "(https://app\\.hubspot\\.com/l/personal-access-key\\S+)",
45700
+ loginNeedsPty: true,
45701
+ credentialPaths: ["~/.hscli/config.yml"],
45702
+ description: "Invoked as `hs`. `hs api <endpoint>` makes authenticated calls to any HubSpot API (contacts, deals, companies) — prefer it over raw curl.",
45703
+ versionCmd: "hs --version 2>/dev/null | sed -n 1p",
45704
+ canStreamLogin: true
45705
+ },
45706
+ "posthog-cli": {
45707
+ preinstallCloud: true,
45708
+ category: "monitoring",
45709
+ pkg: "@posthog/cli",
45710
+ installer: "npm",
45711
+ label: "PostHog",
45712
+ summary: "Query PostHog product analytics and manage feature flags.",
45713
+ statusCmd: `grep -q '"token"' "\${POSTHOG_HOME:-$HOME/.posthog}/credentials.json" 2>/dev/null && echo authenticated`,
45714
+ loginCmd: "posthog-cli login",
45715
+ loginTerminalHint: "Interactive only: run `posthog-cli login` in a terminal on this host (it asks for host, project and a personal API key), or set `POSTHOG_CLI_API_KEY` + `POSTHOG_CLI_PROJECT_ID`.",
45716
+ credentialPaths: ["~/.posthog/credentials.json"],
45717
+ description: "Official PostHog CLI. `posthog-cli api tools` lists the agent-first API tool catalog, `posthog-cli api call <tool> '<json>'` runs one (SQL queries, insights, flags). Also uploads sourcemaps/dSYM. Env alternative: POSTHOG_CLI_API_KEY + POSTHOG_CLI_PROJECT_ID.",
45718
+ versionCmd: "posthog-cli --version 2>/dev/null | sed -n 1p"
45719
+ },
45720
+ canva: {
45721
+ category: "design",
45722
+ pkg: "@canva/cli",
45723
+ installer: "npm",
45724
+ label: "Canva",
45725
+ summary: "Build and deploy Canva apps.",
45726
+ statusCmd: "test -s ~/.canva-cli/credentials && echo authenticated",
45727
+ loginCmd: "canva login --mode manual --lite",
45728
+ loginNeedsPty: true,
45729
+ stdinPaste: true,
45730
+ credentialPaths: ["~/.canva-cli/credentials"],
45731
+ description: "Official Canva CLI — app-development focused: `canva apps create/list/preview`. `canva mcp` starts the Canva Developer MCP server. For design/export automation use the Canva Connect REST API.",
45732
+ versionCmd: "canva --version 2>/dev/null | sed -n 1p",
45733
+ oauthUrlPattern: "OPEN_URL: (https://www\\.canva\\.com/api/oauth/authorize\\S+)"
45734
+ },
45735
+ webflow: {
45736
+ category: "development",
45737
+ pkg: "@webflow/webflow-cli",
45738
+ installer: "npm",
45739
+ label: "Webflow",
45740
+ summary: "Manage Webflow sites, CMS content and assets.",
45741
+ statusCmd: "webflow auth status --skip-update-check >/dev/null 2>&1 && echo authenticated",
45742
+ loginCmd: "webflow auth login --skip-update-check",
45743
+ loginNeedsPty: true,
45744
+ oauthUrlPattern: "(https://webflow\\.com/oauth/webflow/initiate\\S+)",
45745
+ credentialPaths: ["~/.config/webflow"],
45746
+ description: "Manage Webflow sites from the shell: `webflow sites list`, `webflow cms ...`, `webflow assets ...`. Add `--skip-update-check` in scripts.",
45747
+ versionCmd: "webflow --version 2>/dev/null | sed -n 1p"
45748
+ },
45749
+ readwise: {
45750
+ category: "productivity",
45751
+ pkg: "@readwise/cli",
45752
+ installer: "npm",
45753
+ label: "Readwise",
45754
+ summary: "Search and save articles and highlights in Readwise.",
45755
+ statusCmd: "readwise whoami >/dev/null 2>&1 && readwise whoami 2>/dev/null | sed 's/\\x1b\\[[0-9;]*m//g' | grep -m1 .",
45756
+ loginCmd: "readwise login",
45757
+ credentialPaths: ["~/.readwise-cli.json"],
45758
+ description: "Official Readwise/Reader CLI. `readwise search <query>`, `readwise list`, `readwise save <url>`; supports JSON output for piping and a read-only mode for agents. Token login for CI: `readwise login-with-token <token>` (get one at readwise.io/access_token).",
45759
+ versionCmd: "readwise --version 2>/dev/null | sed -n 1p",
45760
+ oauthUrlPattern: "OPEN_URL: (https://readwise\\.io/o/authorize\\S+)",
45761
+ oauthCallbackPort: 6274
45762
+ },
45763
+ granola: {
45764
+ preinstallCloud: true,
45765
+ category: "productivity",
45766
+ pkg: "granola-cli",
45767
+ installer: "npm",
45768
+ binName: "granola",
45769
+ label: "Granola",
45770
+ summary: "Read your Granola meeting notes and transcripts.",
45771
+ statusCmd: `granola auth status -o json 2>/dev/null | sed -n 's/.*"authenticated": *true.*/authenticated/p' | grep -m1 .`,
45772
+ loginCmd: "granola auth login",
45773
+ loginTerminalHint: "Imports the Granola desktop app's session: install Granola on this host, sign in there, then run `granola auth login`.",
45774
+ credentialPaths: ["~/.config/keyring", "~/.config/granola"],
45775
+ versionCmd: "granola --version 2>/dev/null | sed -n 1p",
45776
+ description: "Community Granola CLI (read-only). `granola meeting list [-l N] [--since <date>] [-s <query>] [-a <attendee>] -o json`, `granola meeting notes|enhanced|transcript <id>`, `granola meeting export <id>`; `granola folder list`, `granola workspace list`. `-o json|yaml|toon` for piping. Login imports credentials from the installed Granola desktop app (`granola auth login`) — desktop app must be logged in on the same machine; transcripts need a paid Granola plan."
45777
+ },
45778
+ trello: {
45779
+ preinstallCloud: true,
45780
+ category: "productivity",
45781
+ pkg: "trello-cli",
45782
+ installer: "npm",
45783
+ binName: "trello",
45784
+ label: "Trello",
45785
+ summary: "Manage your Trello boards, lists and cards.",
45786
+ statusCmd: "trello board:list --format json </dev/null >/dev/null 2>&1 && echo authenticated",
45787
+ loginCmd: "trello auth:api-key <key> && trello auth:token <token>",
45788
+ credentialPaths: [
45789
+ "~/.trello-cli"
45790
+ ],
45791
+ versionCmd: "trello --version 2>/dev/null | sed -n 1p",
45792
+ description: "Community Trello CLI (mheap/trello-cli, no official one exists). Auth is a Trello API key + token from https://trello.com/app-key — `trello auth:api-key <key>` then `trello auth:token <token>`. `board:list`, `list:list --board <name>`, `card:list --board <b> --list <l>`, `card:create --board <b> --list <l> --name <n> [--description ..] [--due <date>]`, `card:move`, `card:comment`, `label:*`, `search <q>`. Add `--format json` for piping. Names resolve via a local cache; run `trello board:list` once after login to warm it."
45793
+ },
45794
+ mapps: {
45795
+ category: "development",
45796
+ pkg: "@mondaycom/apps-cli",
45797
+ installer: "npm",
45798
+ label: "monday.com",
45799
+ summary: "Build and deploy monday.com apps.",
45800
+ statusCmd: "grep -qsE '[A-Za-z0-9]' ~/.config/mapps/.mappsrc ~/.mappsrc && echo authenticated",
45801
+ loginCmd: "mapps init -t <api_token>",
45802
+ credentialPaths: ["~/.config/mapps/.mappsrc", "~/.mappsrc"],
45803
+ description: "Invoked as `mapps` — app development and monday-code deployments only; `mapps init -t <token>` stores a monday.com API token in ~/.mappsrc. To read/write board DATA use the monday.com GraphQL API (api.monday.com/v2) with that same token.",
45804
+ versionCmd: "mapps --version 2>/dev/null | sed -n 1p"
45805
+ },
45806
+ "exa-cli": {
45807
+ preinstallCloud: true,
45808
+ category: "utility",
45809
+ pkg: "exa-cli",
45810
+ installer: "npm",
45811
+ label: "Exa Search",
45812
+ summary: "Search the web and pull page contents, built for AI.",
45813
+ binName: "exa",
45814
+ statusCmd: 'test -n "$EXA_API_KEY" && echo authenticated',
45815
+ credentialPaths: [],
45816
+ description: "Invoked as `exa`. `exa search <query>`, `exa answer <question>`, `exa contents <url>`, `exa find-similar <url>` — JSON by default. Needs EXA_API_KEY in the environment (get one at dashboard.exa.ai); store it in the vault.",
45817
+ versionCmd: "exa --version 2>/dev/null | sed -n 1p"
45818
+ },
45819
+ mobbin: {
45820
+ category: "design",
45821
+ pkg: "mobbin",
45822
+ installer: "npm",
45823
+ label: "Mobbin",
45824
+ summary: "Browse real app and website design examples.",
45825
+ statusCmd: "mobbin auth print-cookie 2>/dev/null | grep -E '[A-Za-z0-9]' >/dev/null && echo authenticated",
45826
+ loginCmd: "mobbin auth save-cookie",
45827
+ loginTerminalHint: "Log in to mobbin.com in a browser on this host, then run `mobbin auth save-cookie`: it imports that browser's cookies.",
45828
+ credentialPaths: ["~/.config/mobbin/cookie.txt", "~/.config/mobbin/storage-state.json"],
45829
+ description: 'Unofficial CLI over Mobbin\'s undocumented web endpoints. `mobbin search --query "login" --platform ios`. Auth reuses browser cookies: log into mobbin.com in Chrome, then `mobbin auth save-cookie`.',
45830
+ versionCmd: "mobbin --version 2>/dev/null | sed -n '1,2p' | tr '\\n' ' '"
45831
+ },
45832
+ "blender-mcp": {
45833
+ preinstallCloud: true,
45834
+ packages: ["blender-mcp==1.9.1"],
45835
+ category: "design",
45836
+ pkg: "blender-mcp",
45837
+ installer: "pip",
45838
+ label: "Blender",
45839
+ summary: "Model, texture and render 3D scenes in a running Blender.",
45840
+ statusCmd: `command -v blender-mcp >/dev/null && mcporter call blender.get_addon_status --output json 2>/dev/null | grep -o '"blender_version": "[^"]*"' | grep -m1 .`,
45841
+ loginNoBrowser: true,
45842
+ loginTerminalHint: "Needs Blender (blender.org) open on this host: in Blender, Edit → Preferences → Add-ons, enable `Blender MCP`, then press N in the 3D view → BlenderMCP tab → Connect / Start MCP server. Then press Connect here.",
45843
+ loginCmd: `blender-mcp install-addon >/dev/null 2>&1; command -v mcporter >/dev/null 2>&1 || npm install -g --prefix "$HOME/.local" mcporter; mcporter config add blender --command blender-mcp --env BLENDER_MCP_DISABLE_TELEMETRY=1 --description 'Blender 3D via ahujasid/blender-mcp addon' --scope home`,
45844
+ logoutCmd: "cd ~ && mcporter config remove blender",
45845
+ credentialPaths: ["~/.mcporter/mcporter.json"],
45846
+ description: "Blender 3D via the ahujasid/blender-mcp server through mcporter: `mcporter list blender --schema`, `mcporter call blender.<tool> key=value` (`code=@file.py`; every tool needs `user_prompt`). Workhorse: execute_blender_code (any bpy Python); also scene/object info, viewport screenshot (`--save-images <dir>`), PolyHaven/Sketchfab import. Needs a GUI Blender with the addon server started (socket :9876; headless has none). 'Could not connect' → `blender-mcp install-addon`, enable in Preferences → Add-ons, N-panel → Start MCP Server (scripted: `blender --python s.py`, s.py registers a `bpy.app.timers` calling `bpy.ops.blendermcp.start_server()`). Code runs unguarded: save first (`bpy.ops.wm.save_mainfile()`), approve destructive ops.",
45847
+ versionCmd: `{ python -c 'from importlib.metadata import version; print(version("blender-mcp"))' || python3 -c 'from importlib.metadata import version; print(version("blender-mcp"))'; } 2>/dev/null`
45848
+ },
45849
+ "google-ads": {
45850
+ category: "marketing",
45851
+ pkg: "google-ads",
45852
+ packages: [
45853
+ "google-ads==32.0.0"
45854
+ ],
45855
+ installer: "pip",
45856
+ label: "Google Ads",
45857
+ summary: "Query and manage Google Ads campaigns, keywords and conversions.",
45858
+ loginCmd: `printf 'developer_token: %s\\nclient_id: %s\\nclient_secret: %s\\nrefresh_token: %s\\nlogin_customer_id: %s\\nuse_proto_plus: true\\n' <developer_token> <client_id> <client_secret> <refresh_token> <mcc_id_digits_only> > "$HOME/google-ads.yaml" && python3 -c 'from google.ads.googleads.client import GoogleAdsClient; GoogleAdsClient.load_from_storage(); print("ok")'`,
45859
+ credentialPaths: [
45860
+ "~/google-ads.yaml"
45861
+ ],
45862
+ statusCmd: `python3 -c 'from google.ads.googleads.client import GoogleAdsClient; c=GoogleAdsClient.load_from_storage(); print(c.login_customer_id or "authenticated")' 2>/dev/null | grep -m1 .`,
45863
+ versionCmd: "python3 -c 'import google.ads.googleads as g; print(g.VERSION)' 2>/dev/null",
45864
+ cloudVerifyCmd: "python3 -c 'import google.ads.googleads'",
45865
+ description: "Official Google Ads API client library (Python) — there is no official CLI, so call it via `python3 -c` or a script. Config `~/google-ads.yaml` (developer_token, client_id/secret, refresh_token, login_customer_id = MCC digits). Mint refresh_token with the bundled `google-oauthlib-tool --client-secrets <json> --scope https://www.googleapis.com/auth/adwords --save --headless` (or examples/authentication/generate_user_credentials.py). Read: `client.get_service('GoogleAdsService').search_stream(customer_id=<digits>, query=<GAQL>)`. Write: `<X>Service.mutate_<x>s(customer_id, operations, validate_only=True)` first, then real. Keyword ideas: `KeywordPlanIdeaService.generate_keyword_ideas`. Offline conversions: `ConversionUploadService.upload_click_conversions`. Customer ids are digits without dashes."
45866
+ },
45867
+ "cua-driver": {
45868
+ category: "development",
45869
+ pkg: "cua-driver",
45870
+ installer: "system",
45871
+ label: "Desktop Control",
45872
+ summary: "Click, type and read native desktop apps without taking over the mouse or focus.",
45873
+ installCmd: 'curl -fsSL https://cua.ai/driver/install.sh | CUA_DRIVER_NO_MODIFY_PATH=1 CUA_DRIVER_BIN_DIR="$HOME/.todoforai/tools/bin" bash',
45874
+ uninstallCmd: 'curl -fsSL https://cua.ai/driver/uninstall.sh | bash; rm -f "$HOME/.todoforai/tools/bin/cua-driver" "$HOME/.todoforai/tools/bin/cua-cursor-theme"; hash -r 2>/dev/null; ! command -v cua-driver >/dev/null 2>&1 || { echo "cua-driver is still on PATH: $(command -v cua-driver)"; exit 1; }',
45875
+ installCmdWindows: "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$ProgressPreference='\\''SilentlyContinue'\\''; $ErrorActionPreference='\\''Stop'\\''; [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; $t=Join-Path $env:USERPROFILE '\\''.todoforai\\tools'\\''; $d=Join-Path $t '\\''bin'\\''; New-Item -ItemType Directory -Force -Path $d | Out-Null; $env:CUA_DRIVER_RS_INSTALL_DIR=Join-Path $t '\\''cua-driver\\bin'\\''; $f=Join-Path $env:TEMP '\\''cua-driver-install.ps1'\\''; Invoke-RestMethod -UseBasicParsing https://cua.ai/driver/install.ps1 | Set-Content -LiteralPath $f -Encoding UTF8; & $f -NoAutoStart -NoPathUpdate; if($LASTEXITCODE){exit $LASTEXITCODE}; $u=New-Object Text.UTF8Encoding $false; [IO.File]::WriteAllText((Join-Path $d '\\''cua-driver'\\''), \"#!/bin/sh`nexec `\"`$(dirname `\"`$0`\")/../cua-driver/bin/cua-driver.exe`\" `\"`$@`\"`n\", $u); [IO.File]::WriteAllText((Join-Path $d '\\''cua-driver.cmd'\\''), \"@echo off`r`n`\"%~dp0..\\cua-driver\\bin\\cua-driver.exe`\" %*`r`n\", $u)'",
45876
+ uninstallCmdWindows: "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$ErrorActionPreference='\\''Continue'\\''; $t=Join-Path $env:USERPROFILE '\\''.todoforai\\tools'\\''; $d=Join-Path $t '\\''bin'\\''; $j=Join-Path $t '\\''cua-driver\\bin'\\''; $exe=Join-Path $j '\\''cua-driver.exe'\\''; if(Test-Path -LiteralPath $exe){& $exe stop 2>$null | Out-Null}; Stop-Process -Name cua-driver -Force -ErrorAction SilentlyContinue; $env:CUA_DRIVER_RS_UNINSTALL_FORCE='\\''1'\\''; $env:CUA_DRIVER_RS_INSTALL_DIR=$j; Invoke-RestMethod -UseBasicParsing https://cua.ai/driver/uninstall.ps1 | Invoke-Expression; Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $d '\\''cua-driver'\\''), (Join-Path $d '\\''cua-driver.cmd'\\''); if(Test-Path -LiteralPath $j){[IO.Directory]::Delete($j,$false)}; Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $t '\\''cua-driver'\\'')'",
45877
+ versionCmd: "cua-driver --version 2>/dev/null | sed -n 1p",
45878
+ description: 'Native desktop automation (macOS/Windows/Linux X11; Wayland only via AT-SPI, no window screenshots) through the accessibility tree — actions land in the BACKGROUND without moving the user\'s focus or pointer. Needs a daemon: `cua-driver status || (cua-driver serve --no-overlay >/dev/null 2>&1 &)`; on macOS grant permissions first with `cua-driver permissions grant`; on Windows the daemon must run in the interactive desktop session (`cua-driver autostart enable` registers a logon task). Loop: `cua-driver call list_windows \'{}\'` → pid+window_id; `cua-driver call get_window_state \'{"pid":P,"window_id":W,"include_screenshot":false}\'` → `elements[]` with `element_token`; act with `cua-driver call click \'{"pid":P,"window_id":W,"element_token":"s..:N"}\'`, `type_text {..,"text":".."}`, `hotkey {..,"keys":["ctrl","s"]}`, `press_key`, `scroll`, `invoke_menu`. Tokens are snapshot-bound: re-run get_window_state after EVERY action (stale token = refusal). Window screenshot: add `--screenshot-out-file out.png` (`max_dimension` to shrink). Responses say `effect:unverifiable` — verify from a fresh tree, not from the click reply. `delivery_mode:"foreground"` steals focus; use only when asked. Chrome/Electron trees are near-empty — use agent-browser for web pages. `cua-driver list-tools` / `describe <tool>` for the full surface; `cua-driver mcp` exposes the same over stdio MCP.'
45879
+ }
45880
+ };
45881
+
45882
+ // ../packages/shared-fbe/src/toolCatalog.ts
45883
+ var TOOL_CATALOG = tool_catalog_default;
45884
+ // ../packages/shared-fbe/src/toolInstallCommand.ts
45885
+ var KEY_OF = new Map(Object.entries(TOOL_CATALOG).map(([k, e]) => [e, k]));
45886
+ var NODE_MIN_MAJOR = 24;
45887
+ var NODE_OK = `command -v npm >/dev/null 2>&1 && node -e 'process.exit(+process.versions.node.split(".")[0]>=${NODE_MIN_MAJOR}?0:1)' >/dev/null 2>&1`;
44335
45888
  // ../packages/shared-fbe/src/model_promos.json
44336
45889
  var model_promos_default = {
44337
45890
  _doc: [
@@ -44483,6 +46036,10 @@ var TOPICS = {
44483
46036
  channel: (p) => SERVER_TO_FRONTENDS.todo.message_removed(p.todoId),
44484
46037
  audience: "frontend"
44485
46038
  },
46039
+ ["todo:comment_update" /* TODO_COMMENT_UPDATE */]: {
46040
+ channel: (p) => SERVER_TO_FRONTENDS.todo.commentUpdate(p.todoId),
46041
+ audience: "frontend"
46042
+ },
44486
46043
  ["todo:msg_meta_usr" /* TODO_MSG_META_USR */]: {
44487
46044
  channel: (p) => SERVER_TO_FRONTENDS.todo.msgMetaUsr(p.todoId),
44488
46045
  audience: "frontend"
@@ -44559,7 +46116,7 @@ import { parseArgs } from "util";
44559
46116
  // package.json
44560
46117
  var package_default = {
44561
46118
  name: "@todoforai/cli",
44562
- version: "0.1.54",
46119
+ version: "0.1.56",
44563
46120
  type: "module",
44564
46121
  bin: {
44565
46122
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44627,6 +46184,9 @@ Examples:
44627
46184
  tfa-cli show list|rm … # List / take down shown files
44628
46185
  tfa-cli open <url> [todo-id] # Live url preview in the chat
44629
46186
  tfa-cli recommend --template <id> # Add a template as a recommendation card
46187
+ tfa-cli inbox [needs-you|messages|activity] [--unread] # Read the notification feed (the bell)
46188
+ tfa-cli inbox seen [tab] # Mark notifications read
46189
+ tfa-cli notify "Title" "message" [--href /t/<id>] # Note to YOUR OWN user only (Messages tab + phone push); no --to
44630
46190
  tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> links for a project you own
44631
46191
  tfa-cli next [--direction "<text>"] # Analyzer growth recommendation cards
44632
46192
  tfa-cli <command> help # Details per command
@@ -44707,7 +46267,7 @@ var WORD_FLAGS = {
44707
46267
  agents: "list-agents",
44708
46268
  config: "show-config"
44709
46269
  };
44710
- var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open"];
46270
+ var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open", "inbox", "notify"];
44711
46271
  function parseCliArgs() {
44712
46272
  const { values, positionals } = parseArgs({
44713
46273
  args: process.argv.slice(2),
@@ -44742,6 +46302,10 @@ function parseCliArgs() {
44742
46302
  seed: { type: "string" },
44743
46303
  emails: { type: "string" },
44744
46304
  ttl: { type: "string" },
46305
+ subject: { type: "string" },
46306
+ href: { type: "string" },
46307
+ unread: { type: "boolean", default: false },
46308
+ limit: { type: "string" },
44745
46309
  resume: { type: "string", short: "r" },
44746
46310
  continue: { type: "boolean", short: "c", default: false },
44747
46311
  "non-interactive": { type: "boolean", short: "n", default: false },
@@ -46999,7 +48563,88 @@ function renderDiff(originalContent, modifiedContent, filePath) {
46999
48563
  return parts.join("");
47000
48564
  }
47001
48565
 
48566
+ // src/shutdown.ts
48567
+ var activeRun = null;
48568
+ var cleanups = [];
48569
+ var exiting = null;
48570
+ var cancelling = null;
48571
+ var CANCEL_TIMEOUT_MS = 5000;
48572
+ function setActiveRun(run) {
48573
+ activeRun = run;
48574
+ }
48575
+ function withTimeout(p, ms, fallback) {
48576
+ let timer;
48577
+ const timeout = new Promise((r) => {
48578
+ timer = setTimeout(() => r(fallback), ms);
48579
+ });
48580
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
48581
+ }
48582
+ function cancelActiveRun(timeoutMs = CANCEL_TIMEOUT_MS) {
48583
+ if (cancelling)
48584
+ return cancelling;
48585
+ const pending = activeRun;
48586
+ if (!pending)
48587
+ return Promise.resolve({ ok: false, todoId: null });
48588
+ const res = { ok: false };
48589
+ const attempt = (async () => {
48590
+ const run = await pending;
48591
+ res.todoId = run?.todoId ?? null;
48592
+ if (!run)
48593
+ return res;
48594
+ if (!run.ws.connected && !await run.ws.connect())
48595
+ return res;
48596
+ res.ok = await run.ws.sendInterrupt(run.projectId, run.todoId);
48597
+ return res;
48598
+ })().catch(() => res);
48599
+ cancelling = withTimeout(attempt, timeoutMs, res).finally(() => {
48600
+ if (activeRun === pending)
48601
+ activeRun = null;
48602
+ cancelling = null;
48603
+ });
48604
+ return cancelling;
48605
+ }
48606
+ function shutdown(code, reason) {
48607
+ return exiting ??= (async () => {
48608
+ if (reason)
48609
+ process.stderr.write(`
48610
+ ${reason}
48611
+ `);
48612
+ if (activeRun) {
48613
+ const { ok, todoId } = await cancelActiveRun();
48614
+ if (ok)
48615
+ process.stderr.write(`Stopped todo ${todoId}
48616
+ `);
48617
+ else if (todoId)
48618
+ process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
48619
+ `);
48620
+ else if (todoId === undefined)
48621
+ process.stderr.write(`Warning: could not stop the todo being created (timed out)
48622
+ `);
48623
+ }
48624
+ for (const fn of cleanups.splice(0)) {
48625
+ try {
48626
+ fn();
48627
+ } catch {}
48628
+ }
48629
+ process.exit(code);
48630
+ })();
48631
+ }
48632
+
47002
48633
  // src/watch.ts
48634
+ var RECONNECT_WINDOW_MS = 60000;
48635
+ var DISCONNECTED = { type: "socket:closed", success: false };
48636
+ async function resubscribe(ws, todoId, callback, windowMs = RECONNECT_WINDOW_MS) {
48637
+ const deadline = Date.now() + windowMs;
48638
+ for (let delay = 1000;; delay = Math.min(delay * 2, 5000)) {
48639
+ await new Promise((r) => setTimeout(r, Math.max(0, Math.min(delay, deadline - Date.now()))));
48640
+ const left = deadline - Date.now();
48641
+ if (left <= 0)
48642
+ return false;
48643
+ const attempt = ws.subscribe(todoId, callback, { replayStatus: true }).catch(() => false);
48644
+ if (await withTimeout(attempt, left, false))
48645
+ return true;
48646
+ }
48647
+ }
47003
48648
  var diffStoreByWs = new WeakMap;
47004
48649
  function classifyBlock(info) {
47005
48650
  const inner = (info.block_type || "").toLowerCase();
@@ -47081,12 +48726,8 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
47081
48726
  process.removeAllListeners("SIGINT");
47082
48727
  process.on("SIGINT", () => {
47083
48728
  interruptCount++;
47084
- if (interruptCount >= 2) {
47085
- process.stderr.write(`
47086
- ${RED}Force exit (double Ctrl+C)${RESET}
47087
- `);
47088
- process.exit(130);
47089
- }
48729
+ if (opts.exitOnInterrupt || interruptCount >= 2)
48730
+ return void shutdown(130, "Cancelled by user (Ctrl+C)");
47090
48731
  process.stderr.write(`
47091
48732
  ${YELLOW}Interrupting... (Ctrl+C again to force exit)${RESET}
47092
48733
  `);
@@ -47285,8 +48926,22 @@ ${DIM}[todo:status] ${status}${RESET}
47285
48926
  callback(msgType, payload);
47286
48927
  }
47287
48928
  }
48929
+ setActiveRun({ ws, projectId, todoId });
47288
48930
  try {
47289
- const result = await ws.waitForCompletion(todoId, callback);
48931
+ const subscribed = await ws.subscribe(todoId, callback).catch(() => false);
48932
+ let result = subscribed ? await ws.completion(todoId) : DISCONNECTED;
48933
+ while (result?.type === "socket:closed") {
48934
+ process.stderr.write(`
48935
+ ${YELLOW}Connection lost — reconnecting...${RESET}
48936
+ `);
48937
+ if (!await resubscribe(ws, todoId, callback)) {
48938
+ await shutdown(1, `${RED}Error: backend unreachable for ${RECONNECT_WINDOW_MS / 1000}s — giving up on todo ${todoId}${RESET}`);
48939
+ }
48940
+ process.stderr.write(`${DIM}Reconnected.${RESET}
48941
+ `);
48942
+ result = await ws.completion(todoId);
48943
+ }
48944
+ setActiveRun(null);
47290
48945
  process.stdout.write(`
47291
48946
  `);
47292
48947
  if (!result?.success) {
@@ -47299,6 +48954,8 @@ ${DIM}[todo:status] ${status}${RESET}
47299
48954
  }
47300
48955
  return true;
47301
48956
  } catch (e) {
48957
+ await cancelActiveRun();
48958
+ process.exitCode = 1;
47302
48959
  if (!opts.suppressCancelNotice) {
47303
48960
  process.stderr.write(`${YELLOW}Interrupted${RESET}
47304
48961
  `);
@@ -48362,210 +50019,629 @@ async function deviceCommand(api, positionals, args) {
48362
50019
  await api.renameDevice(found.id, value);
48363
50020
  process.stderr.write(`${GREEN}✅ ${found.name} → ${value}${RESET}
48364
50021
  `);
48365
- return;
50022
+ return;
50023
+ }
50024
+ if (sub === "wallpaper") {
50025
+ if (value !== "auto" && value !== "off")
50026
+ fail("Usage: tfa-cli device wallpaper <device> auto|off");
50027
+ await api.setDeviceWallpaperMode(found.id, value);
50028
+ process.stderr.write(`${GREEN}✅ ${found.name} wallpaper: ${value}${RESET}
50029
+ `);
50030
+ return;
50031
+ }
50032
+ fail(`Unknown 'device' subcommand: ${sub}`);
50033
+ }
50034
+
50035
+ // src/list-todos.ts
50036
+ import { parseArgs as parseArgs2 } from "util";
50037
+ var STATUS_COLOR = {
50038
+ DONE: GREEN,
50039
+ READY: GREEN,
50040
+ READY_CHECKED: GREEN,
50041
+ ERROR: RED,
50042
+ ERROR_CHECKED: RED,
50043
+ CANCELLED: RED,
50044
+ CANCELLED_CHECKED: RED,
50045
+ RUNNING: YELLOW,
50046
+ STOPPING: YELLOW,
50047
+ COMPACTING: YELLOW,
50048
+ REVIEW_REQUESTED: YELLOW,
50049
+ TODO: CYAN,
50050
+ SCHEDULED: CYAN,
50051
+ POSTPONED: CYAN,
50052
+ PAUSED: CYAN
50053
+ };
50054
+ var CLOSED = new Set([
50055
+ "DONE",
50056
+ "READY_CHECKED",
50057
+ "CANCELLED",
50058
+ "CANCELLED_CHECKED",
50059
+ "ERROR_CHECKED",
50060
+ "ARCHIVED",
50061
+ "DELETED"
50062
+ ]);
50063
+ var VALID_STATUS = new Set(Object.values(TodoStatus));
50064
+ var isOpen = (s) => !CLOSED.has(s);
50065
+ function printListTodosHelp() {
50066
+ process.stderr.write(`
50067
+ todoforai-cli list — list todos in a project (recent first)
50068
+
50069
+ Usage:
50070
+ todoforai-cli list [flags]
50071
+
50072
+ Flags:
50073
+ -n, --limit <n> Max rows to show (default: 30)
50074
+ --cursor <n> Fetch todos older than this cursor (lastActivityAt)
50075
+ --page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
50076
+ --search <text> Search content/status on the backend
50077
+ -s, --status <S[,S2]> Filter by status (comma-separated, union).
50078
+ -A, --all Include DONE (also CANCELLED, ARCHIVED, …)
50079
+ --project <id> Project ID (default: current default project)
50080
+ --json Output { items, nextCursor } as JSON
50081
+ -h, --help Show this help
50082
+
50083
+ Examples:
50084
+ todoforai-cli list # 30 most recent open todos
50085
+ todoforai-cli list -n 50 # last 50 open
50086
+ todoforai-cli list --cursor 1719234567890 # next page
50087
+ todoforai-cli list --all # include DONE
50088
+ todoforai-cli list -s RUNNING,REVIEW_REQUESTED
50089
+ todoforai-cli list --search bug --json | jq '.items[].id'
50090
+ `);
50091
+ }
50092
+ function positiveInt(value, name) {
50093
+ if (value === undefined)
50094
+ return;
50095
+ const n = Number(value);
50096
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
50097
+ process.stderr.write(`${RED}Error: ${name} must be a positive integer${RESET}
50098
+ `);
50099
+ process.exit(2);
50100
+ }
50101
+ return n;
50102
+ }
50103
+ function cursorValue(value) {
50104
+ if (value === undefined)
50105
+ return;
50106
+ const n = Number(value);
50107
+ if (!Number.isFinite(n) || n < 0) {
50108
+ process.stderr.write(`${RED}Error: --cursor must be a non-negative number${RESET}
50109
+ `);
50110
+ process.exit(2);
50111
+ }
50112
+ return n;
50113
+ }
50114
+ function toItemsPage(response) {
50115
+ if (Array.isArray(response))
50116
+ return { items: response };
50117
+ return {
50118
+ items: Array.isArray(response?.items) ? response.items : [],
50119
+ nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
50120
+ };
50121
+ }
50122
+ function rowTime(t) {
50123
+ return Number(t.lastActivityAt ?? t.createdAt ?? 0);
50124
+ }
50125
+ function printStatusError(unknown) {
50126
+ process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
50127
+ `);
50128
+ process.stderr.write(`${DIM}Use OPEN or one of: ${Object.values(TodoStatus).join(", ")}${RESET}
50129
+ `);
50130
+ process.exit(2);
50131
+ }
50132
+ function matchesRequestedStatus(t, requested, wanted, wantOpen) {
50133
+ if (!requested)
50134
+ return true;
50135
+ const s = String(t.status).toUpperCase();
50136
+ return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
50137
+ }
50138
+ async function listTodosCommand(api, defaultProjectId, argv) {
50139
+ const { values } = parseArgs2({
50140
+ args: argv,
50141
+ options: {
50142
+ limit: { type: "string", short: "n" },
50143
+ cursor: { type: "string" },
50144
+ "page-size": { type: "string" },
50145
+ search: { type: "string" },
50146
+ status: { type: "string", short: "s" },
50147
+ all: { type: "boolean", short: "A", default: false },
50148
+ project: { type: "string" },
50149
+ json: { type: "boolean", default: false },
50150
+ help: { type: "boolean", short: "h", default: false }
50151
+ },
50152
+ strict: true
50153
+ });
50154
+ if (values.help) {
50155
+ printListTodosHelp();
50156
+ return;
50157
+ }
50158
+ const projectId = values.project || defaultProjectId;
50159
+ if (!projectId) {
50160
+ process.stderr.write(`${RED}Error: no project (pass --project <id> or set a default)${RESET}
50161
+ `);
50162
+ process.exit(1);
50163
+ }
50164
+ const limit = positiveInt(values.limit, "--limit") ?? 30;
50165
+ const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
50166
+ let cursor = cursorValue(values.cursor);
50167
+ const search = values.search ? String(values.search) : undefined;
50168
+ const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
50169
+ const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
50170
+ if (unknown.length)
50171
+ printStatusError(unknown);
50172
+ const wantOpen = !!requested?.some((s) => s === "OPEN");
50173
+ const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
50174
+ const includeClosed = !!values.all || !!wanted && wanted.size > 0;
50175
+ const rows = [];
50176
+ let backendNextCursor;
50177
+ while (rows.length < limit) {
50178
+ const requestLimit = Math.min(pageSize, limit - rows.length);
50179
+ const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
50180
+ backendNextCursor = page.nextCursor;
50181
+ for (const t of page.items) {
50182
+ const status = String(t.status).toUpperCase();
50183
+ if (!includeClosed && CLOSED.has(status))
50184
+ continue;
50185
+ if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
50186
+ continue;
50187
+ rows.push(t);
50188
+ if (rows.length >= limit)
50189
+ break;
50190
+ }
50191
+ if (!backendNextCursor || page.items.length === 0)
50192
+ break;
50193
+ cursor = backendNextCursor;
50194
+ }
50195
+ rows.sort((a, b) => rowTime(b) - rowTime(a));
50196
+ const visible = rows.slice(0, limit);
50197
+ const hasBufferedRows = rows.length > visible.length;
50198
+ const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
50199
+ if (values.json) {
50200
+ process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
50201
+ `);
50202
+ return;
50203
+ }
50204
+ if (!visible.length) {
50205
+ process.stderr.write(`${DIM}(no todos)${RESET}
50206
+ `);
50207
+ if (nextCursor !== undefined)
50208
+ process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
50209
+ `);
50210
+ return;
50211
+ }
50212
+ const statusW = visible.reduce((n, r) => Math.max(n, String(r.status).length), 0);
50213
+ for (const t of visible) {
50214
+ const status = String(t.status).toUpperCase();
50215
+ const sc = STATUS_COLOR[status] || DIM;
50216
+ const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
50217
+ const title = String(t.content ?? "").split(`
50218
+ `)[0].slice(0, 100);
50219
+ process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
50220
+ `);
50221
+ }
50222
+ process.stderr.write(`${DIM}${visible.length} todo(s)${RESET}
50223
+ `);
50224
+ if (nextCursor !== undefined)
50225
+ process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
50226
+ `);
50227
+ }
50228
+
50229
+ // src/notify-command.ts
50230
+ import { createHash } from "crypto";
50231
+
50232
+ // src/input.ts
50233
+ function readMultiline2(prompt, history) {
50234
+ let cancelFn = () => {};
50235
+ const promise = new Promise((resolve4, reject) => {
50236
+ const out = process.stderr;
50237
+ let buf = "";
50238
+ let cursor = 0;
50239
+ let pasting = false;
50240
+ let done = false;
50241
+ let screenRow = 0;
50242
+ let historyIndex = -1;
50243
+ let savedCurrent = "";
50244
+ const promptLen = prompt.replace(/\x1b\[[0-9;]*m/g, "").length;
50245
+ out.write(prompt);
50246
+ process.stdin.setRawMode(true);
50247
+ process.stdin.resume();
50248
+ out.write("\x1B[?2004h");
50249
+ function rowOf(pos) {
50250
+ const cols = process.stderr.columns || 80;
50251
+ const lines = buf.slice(0, pos).split(`
50252
+ `);
50253
+ let row = 0;
50254
+ for (let i = 0;i < lines.length; i++) {
50255
+ const len = (i === 0 ? promptLen : 0) + lines[i].length;
50256
+ if (i < lines.length - 1) {
50257
+ row += len === 0 ? 1 : Math.ceil(len / cols);
50258
+ } else {
50259
+ row += len > 0 ? Math.ceil(len / cols) - 1 : 0;
50260
+ }
50261
+ }
50262
+ return row;
50263
+ }
50264
+ function colOf(pos) {
50265
+ const cols = process.stderr.columns || 80;
50266
+ const lastNl = buf.lastIndexOf(`
50267
+ `, pos - 1);
50268
+ const lineStart = lastNl + 1;
50269
+ const offset = lineStart === 0 ? promptLen : 0;
50270
+ const linePos = offset + (pos - lineStart);
50271
+ if (linePos > 0 && linePos % cols === 0)
50272
+ return cols;
50273
+ return linePos % cols;
50274
+ }
50275
+ function totalRows() {
50276
+ const cols = process.stderr.columns || 80;
50277
+ const lines = buf.split(`
50278
+ `);
50279
+ let rows = 0;
50280
+ for (let i = 0;i < lines.length; i++) {
50281
+ const len = (i === 0 ? promptLen : 0) + lines[i].length;
50282
+ rows += len === 0 ? 1 : Math.ceil(len / cols);
50283
+ }
50284
+ return rows;
50285
+ }
50286
+ function redraw() {
50287
+ const cols = process.stderr.columns || 80;
50288
+ out.write("\r");
50289
+ if (screenRow > 0)
50290
+ out.write(`\x1B[${screenRow}A`);
50291
+ out.write("\x1B[J");
50292
+ out.write(prompt + buf.replace(/\n/g, `\r
50293
+ `));
50294
+ const targetRow = rowOf(cursor);
50295
+ const endRow = totalRows() - 1;
50296
+ const rowsBack = endRow - targetRow;
50297
+ const col = colOf(cursor);
50298
+ if (rowsBack > 0)
50299
+ out.write(`\x1B[${rowsBack}A`);
50300
+ if (col <= cols) {
50301
+ out.write("\r");
50302
+ if (col > 0 && col < cols)
50303
+ out.write(`\x1B[${col}C`);
50304
+ if (col === cols)
50305
+ out.write(`\x1B[${cols}G`);
50306
+ }
50307
+ screenRow = targetRow;
50308
+ }
50309
+ function finish(cancelled) {
50310
+ if (done)
50311
+ return;
50312
+ done = true;
50313
+ const rowsDown = totalRows() - 1 - screenRow;
50314
+ if (rowsDown > 0)
50315
+ out.write(`\x1B[${rowsDown}B`);
50316
+ out.write("\x1B[?2004l");
50317
+ out.write(`
50318
+ `);
50319
+ process.stdin.setRawMode(false);
50320
+ process.stdin.pause();
50321
+ process.stdin.removeListener("data", onData);
50322
+ if (cancelled)
50323
+ reject(new Error("cancelled"));
50324
+ else
50325
+ resolve4(buf.trim());
50326
+ }
50327
+ function wordRight() {
50328
+ let p = cursor;
50329
+ while (p < buf.length && buf[p] === " ")
50330
+ p++;
50331
+ while (p < buf.length && buf[p] !== " ")
50332
+ p++;
50333
+ return p;
50334
+ }
50335
+ function wordLeft() {
50336
+ let p = cursor;
50337
+ while (p > 0 && buf[p - 1] === " ")
50338
+ p--;
50339
+ while (p > 0 && buf[p - 1] !== " ")
50340
+ p--;
50341
+ return p;
50342
+ }
50343
+ function killToEnd() {
50344
+ buf = buf.slice(0, cursor);
50345
+ redraw();
50346
+ }
50347
+ function deleteWordBack() {
50348
+ const to = wordLeft();
50349
+ if (to === cursor)
50350
+ return;
50351
+ buf = buf.slice(0, to) + buf.slice(cursor);
50352
+ cursor = to;
50353
+ redraw();
50354
+ }
50355
+ function handleCSI(chunk, start) {
50356
+ let i = start + 1;
50357
+ if (i >= chunk.length || chunk[i] !== "[") {
50358
+ if (i < chunk.length)
50359
+ i++;
50360
+ return i;
50361
+ }
50362
+ i++;
50363
+ let params = "";
50364
+ while (i < chunk.length && /[0-9;]/.test(chunk[i])) {
50365
+ params += chunk[i];
50366
+ i++;
50367
+ }
50368
+ const final = i < chunk.length ? chunk[i] : "";
50369
+ i++;
50370
+ const parts = params.split(";");
50371
+ const modifier = parts.length > 1 ? parseInt(parts[1]) : 0;
50372
+ const code = parts[0] || "";
50373
+ const ctrl = modifier === 5;
50374
+ switch (final) {
50375
+ case "D":
50376
+ if (ctrl) {
50377
+ cursor = wordLeft();
50378
+ redraw();
50379
+ } else if (cursor > 0) {
50380
+ cursor--;
50381
+ redraw();
50382
+ }
50383
+ break;
50384
+ case "C":
50385
+ if (ctrl) {
50386
+ cursor = wordRight();
50387
+ redraw();
50388
+ } else if (cursor < buf.length) {
50389
+ cursor++;
50390
+ redraw();
50391
+ }
50392
+ break;
50393
+ case "A":
50394
+ if (history && history.length > 0) {
50395
+ if (historyIndex === -1)
50396
+ savedCurrent = buf;
50397
+ if (historyIndex < history.length - 1) {
50398
+ historyIndex++;
50399
+ buf = history[history.length - 1 - historyIndex];
50400
+ cursor = buf.length;
50401
+ redraw();
50402
+ }
50403
+ }
50404
+ break;
50405
+ case "B":
50406
+ if (history && historyIndex >= 0) {
50407
+ historyIndex--;
50408
+ if (historyIndex === -1) {
50409
+ buf = savedCurrent;
50410
+ } else {
50411
+ buf = history[history.length - 1 - historyIndex];
50412
+ }
50413
+ cursor = buf.length;
50414
+ redraw();
50415
+ }
50416
+ break;
50417
+ case "H":
50418
+ cursor = 0;
50419
+ redraw();
50420
+ break;
50421
+ case "F":
50422
+ cursor = buf.length;
50423
+ redraw();
50424
+ break;
50425
+ case "~":
50426
+ if (code === "3" && cursor < buf.length) {
50427
+ buf = buf.slice(0, cursor) + buf.slice(cursor + 1);
50428
+ redraw();
50429
+ }
50430
+ break;
50431
+ }
50432
+ return i;
50433
+ }
50434
+ function onData(data) {
50435
+ let s = data.toString("utf-8");
50436
+ while (s.length > 0 && !done) {
50437
+ if (pasting) {
50438
+ const end = s.indexOf("\x1B[201~");
50439
+ if (end >= 0) {
50440
+ const text = s.slice(0, end).replace(/\r\n?|\n/g, `
50441
+ `);
50442
+ buf = buf.slice(0, cursor) + text + buf.slice(cursor);
50443
+ cursor += text.length;
50444
+ pasting = false;
50445
+ s = s.slice(end + 6);
50446
+ redraw();
50447
+ } else {
50448
+ const text = s.replace(/\r\n?|\n/g, `
50449
+ `);
50450
+ buf = buf.slice(0, cursor) + text + buf.slice(cursor);
50451
+ cursor += text.length;
50452
+ s = "";
50453
+ redraw();
50454
+ }
50455
+ } else {
50456
+ const ps = s.indexOf("\x1B[200~");
50457
+ const chunk = ps >= 0 ? s.slice(0, ps) : s;
50458
+ for (let i = 0;i < chunk.length && !done; i++) {
50459
+ const c3 = chunk.charCodeAt(i);
50460
+ if (c3 === 3) {
50461
+ finish(true);
50462
+ return;
50463
+ }
50464
+ if (c3 === 13 || c3 === 10) {
50465
+ finish(false);
50466
+ return;
50467
+ }
50468
+ if (c3 === 23) {
50469
+ deleteWordBack();
50470
+ } else if (c3 === 11) {
50471
+ killToEnd();
50472
+ } else if (c3 === 127 || c3 === 8) {
50473
+ if (cursor > 0) {
50474
+ buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
50475
+ cursor--;
50476
+ redraw();
50477
+ }
50478
+ } else if (c3 === 27) {
50479
+ if (i + 1 < chunk.length && chunk.charCodeAt(i + 1) === 13) {
50480
+ buf = buf.slice(0, cursor) + `
50481
+ ` + buf.slice(cursor);
50482
+ cursor++;
50483
+ i++;
50484
+ redraw();
50485
+ } else {
50486
+ i = handleCSI(chunk, i) - 1;
50487
+ }
50488
+ } else if (c3 === 1) {
50489
+ cursor = 0;
50490
+ redraw();
50491
+ } else if (c3 === 5) {
50492
+ cursor = buf.length;
50493
+ redraw();
50494
+ } else if (c3 >= 32) {
50495
+ buf = buf.slice(0, cursor) + chunk[i] + buf.slice(cursor);
50496
+ cursor++;
50497
+ redraw();
50498
+ }
50499
+ }
50500
+ if (ps >= 0) {
50501
+ pasting = true;
50502
+ s = s.slice(ps + 6);
50503
+ } else
50504
+ s = "";
50505
+ }
50506
+ }
50507
+ }
50508
+ process.stdin.on("data", onData);
50509
+ cancelFn = () => finish(true);
50510
+ });
50511
+ return { promise, cancel: () => cancelFn() };
50512
+ }
50513
+ async function readStdin2() {
50514
+ if (process.stdin.isTTY) {
50515
+ try {
50516
+ const content2 = await readMultiline2(`${BRIGHT_WHITE}TODO>${RESET} `).promise;
50517
+ if (!content2) {
50518
+ process.stderr.write(`Error: Empty input
50519
+ `);
50520
+ process.exit(1);
50521
+ }
50522
+ return content2;
50523
+ } catch {
50524
+ process.stderr.write(`
50525
+ Cancelled
50526
+ `);
50527
+ process.exit(1);
50528
+ }
48366
50529
  }
48367
- if (sub === "wallpaper") {
48368
- if (value !== "auto" && value !== "off")
48369
- fail("Usage: tfa-cli device wallpaper <device> auto|off");
48370
- await api.setDeviceWallpaperMode(found.id, value);
48371
- process.stderr.write(`${GREEN}✅ ${found.name} wallpaper: ${value}${RESET}
50530
+ const chunks = [];
50531
+ for await (const chunk of process.stdin)
50532
+ chunks.push(chunk);
50533
+ const content = Buffer.concat(chunks).toString("utf-8").trim();
50534
+ if (!content) {
50535
+ process.stderr.write(`Error: Empty input
48372
50536
  `);
48373
- return;
50537
+ process.exit(1);
48374
50538
  }
48375
- fail(`Unknown 'device' subcommand: ${sub}`);
50539
+ return content;
48376
50540
  }
48377
50541
 
48378
- // src/list-todos.ts
48379
- import { parseArgs as parseArgs2 } from "util";
48380
- var STATUS_COLOR = {
48381
- DONE: GREEN,
48382
- READY: GREEN,
48383
- READY_CHECKED: GREEN,
48384
- ERROR: RED,
48385
- ERROR_CHECKED: RED,
48386
- CANCELLED: RED,
48387
- CANCELLED_CHECKED: RED,
48388
- RUNNING: YELLOW,
48389
- STOPPING: YELLOW,
48390
- COMPACTING: YELLOW,
48391
- REVIEW_REQUESTED: YELLOW,
48392
- TODO: CYAN,
48393
- SCHEDULED: CYAN,
48394
- POSTPONED: CYAN,
48395
- PAUSED: CYAN
48396
- };
48397
- var CLOSED = new Set([
48398
- "DONE",
48399
- "READY_CHECKED",
48400
- "CANCELLED",
48401
- "CANCELLED_CHECKED",
48402
- "ERROR_CHECKED",
48403
- "ARCHIVED",
48404
- "DELETED"
48405
- ]);
48406
- var VALID_STATUS = new Set(Object.values(TodoStatus));
48407
- var isOpen = (s) => !CLOSED.has(s);
48408
- function printListTodosHelp() {
50542
+ // src/notify-command.ts
50543
+ var TIERS = { "needs-you": "priority", priority: "priority", activity: "activity", messages: "user_messages" };
50544
+ function printInboxHelp() {
48409
50545
  process.stderr.write(`
48410
- todoforai-cli list — list todos in a project (recent first)
50546
+ tfa-cli inbox — the user's notification feed (the bell)
48411
50547
 
48412
50548
  Usage:
48413
- todoforai-cli list [flags]
48414
-
48415
- Flags:
48416
- -n, --limit <n> Max rows to show (default: 30)
48417
- --cursor <n> Fetch todos older than this cursor (lastActivityAt)
48418
- --page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
48419
- --search <text> Search content/status on the backend
48420
- -s, --status <S[,S2]> Filter by status (comma-separated, union).
48421
- -A, --all Include DONE (also CANCELLED, ARCHIVED, …)
48422
- --project <id> Project ID (default: current default project)
48423
- --json Output { items, nextCursor } as JSON
48424
- -h, --help Show this help
50549
+ tfa-cli inbox [needs-you|messages|activity] [--unread] [--limit 20] [--json]
50550
+ tfa-cli inbox seen [needs-you|messages|activity] Mark read (omit = everything)
48425
50551
 
48426
- Examples:
48427
- todoforai-cli list # 30 most recent open todos
48428
- todoforai-cli list -n 50 # last 50 open
48429
- todoforai-cli list --cursor 1719234567890 # next page
48430
- todoforai-cli list --all # include DONE
48431
- todoforai-cli list -s RUNNING,REVIEW_REQUESTED
48432
- todoforai-cli list --search bug --json | jq '.items[].id'
50552
+ Filters (same as the bell's chips):
50553
+ needs-you invites, offers, a todo waiting on the user, a failed todo (alias: priority)
50554
+ messages notes from the TODO for AI team or from an agent
50555
+ activity finished / recommended todos
48433
50556
  `);
48434
50557
  }
48435
- function positiveInt(value, name) {
48436
- if (value === undefined)
48437
- return;
48438
- const n = Number(value);
48439
- if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
48440
- process.stderr.write(`${RED}Error: ${name} must be a positive integer${RESET}
50558
+ function printNotifyHelp() {
50559
+ process.stderr.write(`
50560
+ tfa-cli notify — leave a note for YOUR user (their Messages tab + phone push)
50561
+
50562
+ Usage:
50563
+ tfa-cli notify "Title" "message" [--href /t/<todo-id>] [--subject <key>] [--json]
50564
+ echo "message" | tfa-cli notify "Title"
50565
+
50566
+ Recipient: ALWAYS the user this key belongs to (the person you work for). There is no --to:
50567
+ it cannot message a teammate, a customer or any other account. Shown as from your agent.
50568
+ --subject dedupes: the same subject is delivered once (default: hash of title+message,
50569
+ so a retry never double-notifies). --href must be an in-app path. Max 10 new notes/day.
48441
50570
  `);
48442
- process.exit(2);
48443
- }
48444
- return n;
48445
50571
  }
48446
- function cursorValue(value) {
48447
- if (value === undefined)
50572
+ function tierArg(name) {
50573
+ if (!name)
48448
50574
  return;
48449
- const n = Number(value);
48450
- if (!Number.isFinite(n) || n < 0) {
48451
- process.stderr.write(`${RED}Error: --cursor must be a non-negative number${RESET}
50575
+ const tier = TIERS[name];
50576
+ if (!tier) {
50577
+ process.stderr.write(`${RED}Unknown tab "${name}" — use needs-you, messages, activity${RESET}
48452
50578
  `);
48453
50579
  process.exit(2);
48454
50580
  }
48455
- return n;
48456
- }
48457
- function toItemsPage(response) {
48458
- if (Array.isArray(response))
48459
- return { items: response };
48460
- return {
48461
- items: Array.isArray(response?.items) ? response.items : [],
48462
- nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
48463
- };
50581
+ return tier;
48464
50582
  }
48465
- function rowTime(t) {
48466
- return Number(t.lastActivityAt ?? t.createdAt ?? 0);
48467
- }
48468
- function printStatusError(unknown) {
48469
- process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
48470
- `);
48471
- process.stderr.write(`${DIM}Use OPEN or one of: ${Object.values(TodoStatus).join(", ")}${RESET}
50583
+ var when = (ms) => new Date(ms).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" });
50584
+ async function inboxCommand(api, positionals, args) {
50585
+ const [, first, second] = positionals;
50586
+ if (first === "seen") {
50587
+ const tier2 = tierArg(second);
50588
+ await api.markFeedSeen(tier2 ? { tier: tier2 } : {});
50589
+ process.stderr.write(`${GREEN}✅ Marked ${second ?? "everything"} read${RESET}
48472
50590
  `);
48473
- process.exit(2);
48474
- }
48475
- function matchesRequestedStatus(t, requested, wanted, wantOpen) {
48476
- if (!requested)
48477
- return true;
48478
- const s = String(t.status).toUpperCase();
48479
- return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
48480
- }
48481
- async function listTodosCommand(api, defaultProjectId, argv) {
48482
- const { values } = parseArgs2({
48483
- args: argv,
48484
- options: {
48485
- limit: { type: "string", short: "n" },
48486
- cursor: { type: "string" },
48487
- "page-size": { type: "string" },
48488
- search: { type: "string" },
48489
- status: { type: "string", short: "s" },
48490
- all: { type: "boolean", short: "A", default: false },
48491
- project: { type: "string" },
48492
- json: { type: "boolean", default: false },
48493
- help: { type: "boolean", short: "h", default: false }
48494
- },
48495
- strict: true
48496
- });
48497
- if (values.help) {
48498
- printListTodosHelp();
48499
50591
  return;
48500
50592
  }
48501
- const projectId = values.project || defaultProjectId;
48502
- if (!projectId) {
48503
- process.stderr.write(`${RED}Error: no project (pass --project <id> or set a default)${RESET}
48504
- `);
48505
- process.exit(1);
48506
- }
48507
- const limit = positiveInt(values.limit, "--limit") ?? 30;
48508
- const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
48509
- let cursor = cursorValue(values.cursor);
48510
- const search = values.search ? String(values.search) : undefined;
48511
- const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
48512
- const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
48513
- if (unknown.length)
48514
- printStatusError(unknown);
48515
- const wantOpen = !!requested?.some((s) => s === "OPEN");
48516
- const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
48517
- const includeClosed = !!values.all || !!wanted && wanted.size > 0;
48518
- const rows = [];
48519
- let backendNextCursor;
48520
- while (rows.length < limit) {
48521
- const requestLimit = Math.min(pageSize, limit - rows.length);
48522
- const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
48523
- backendNextCursor = page.nextCursor;
48524
- for (const t of page.items) {
48525
- const status = String(t.status).toUpperCase();
48526
- if (!includeClosed && CLOSED.has(status))
48527
- continue;
48528
- if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
48529
- continue;
48530
- rows.push(t);
48531
- if (rows.length >= limit)
48532
- break;
48533
- }
48534
- if (!backendNextCursor || page.items.length === 0)
48535
- break;
48536
- cursor = backendNextCursor;
48537
- }
48538
- rows.sort((a, b) => rowTime(b) - rowTime(a));
48539
- const visible = rows.slice(0, limit);
48540
- const hasBufferedRows = rows.length > visible.length;
48541
- const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
48542
- if (values.json) {
48543
- process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
48544
- `);
50593
+ const tier = tierArg(first);
50594
+ const limit = args.limit ? Number(args.limit) : 20;
50595
+ const page = await api.listFeed({ tier, limit, sinceSeen: !!args.unread || undefined });
50596
+ if (args.json) {
50597
+ console.log(JSON.stringify(page));
48545
50598
  return;
48546
50599
  }
48547
- if (!visible.length) {
48548
- process.stderr.write(`${DIM}(no todos)${RESET}
50600
+ const counts = page.unseenByTier;
50601
+ if (counts)
50602
+ process.stderr.write(`${DIM}unread — needs you ${counts.priority} · messages ${counts.user_messages} · activity ${counts.activity}${RESET}
48549
50603
  `);
48550
- if (nextCursor !== undefined)
48551
- process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
50604
+ if (!page.items.length) {
50605
+ process.stderr.write(`${DIM}Nothing here${RESET}
48552
50606
  `);
48553
50607
  return;
48554
50608
  }
48555
- const statusW = visible.reduce((n, r) => Math.max(n, String(r.status).length), 0);
48556
- for (const t of visible) {
48557
- const status = String(t.status).toUpperCase();
48558
- const sc = STATUS_COLOR[status] || DIM;
48559
- const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
48560
- const title = String(t.content ?? "").split(`
48561
- `)[0].slice(0, 100);
48562
- process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
50609
+ for (const e of page.items) {
50610
+ const from = e.sender ? `${CYAN}${senderLabel(e.sender)}${RESET} ` : "";
50611
+ const dot = e.unread ? `${YELLOW}●${RESET}` : " ";
50612
+ const state = e.resolution ? ` ${DIM}[${e.resolution}]${RESET}` : "";
50613
+ console.log(`${dot} ${DIM}${when(e.createdAt)}${RESET} ${from}${BOLD}${e.title ?? e.message}${RESET}${state}`);
50614
+ if (e.title && e.message)
50615
+ console.log(` ${e.message.replace(/\n/g, `
50616
+ `)}`);
50617
+ if (e.href)
50618
+ console.log(` ${DIM}${e.href}${RESET}`);
50619
+ }
50620
+ }
50621
+ var RECIPIENT_FLAGS = ["--to", "--email", "--user", "--recipient", "--cc"];
50622
+ async function notifyCommand(api, positionals, args) {
50623
+ const argv = process.argv.slice(2);
50624
+ const bad = argv.find((a) => RECIPIENT_FLAGS.some((f) => a === f || a.startsWith(`${f}=`)));
50625
+ if (bad) {
50626
+ process.stderr.write(`${RED}notify has no recipient option (${bad.split("=")[0]}): it only reaches the user this key belongs to — your own user. Messaging other people is not supported.${RESET}
48563
50627
  `);
50628
+ process.exit(2);
48564
50629
  }
48565
- process.stderr.write(`${DIM}${visible.length} todo(s)${RESET}
48566
- `);
48567
- if (nextCursor !== undefined)
48568
- process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
50630
+ const [, title, ...rest] = positionals;
50631
+ const message = rest.join(" ") || (process.stdin.isTTY ? "" : (await readStdin2()).trim());
50632
+ if (!title || !message) {
50633
+ printNotifyHelp();
50634
+ process.exit(2);
50635
+ }
50636
+ const subject = args.subject || createHash("sha256").update(`${title}
50637
+ ${message}`).digest("hex").slice(0, 16);
50638
+ const row = await api.sendFeedMessage({ subject, title, message, ...args.href ? { href: String(args.href) } : {} });
50639
+ if (args.json) {
50640
+ console.log(JSON.stringify(row));
50641
+ return;
50642
+ }
50643
+ process.stderr.write(row.created !== false ? `${GREEN}✅ Delivered to your own user's Messages inbox (not anyone else)${RESET} ${DIM}(subject ${subject})${RESET}
50644
+ ` : `${DIM}Already sent (subject ${subject}) — nothing new delivered${RESET}
48569
50645
  `);
48570
50646
  }
48571
50647
 
@@ -48833,7 +50909,7 @@ async function spawnMayflyBridge(apiUrl, todoId, workspace, opts = {}) {
48833
50909
  }
48834
50910
  });
48835
50911
  });
48836
- return { child, stop };
50912
+ return { child, stop, stopped: () => stopped };
48837
50913
  }
48838
50914
 
48839
50915
  // src/acp.ts
@@ -72898,6 +74974,93 @@ async function runAcp(apiUrl, apiKey, opts) {
72898
74974
  await agent2.close();
72899
74975
  }
72900
74976
 
74977
+ // src/shutdown.ts
74978
+ var activeRun2 = null;
74979
+ var cleanups2 = [];
74980
+ var exiting2 = null;
74981
+ var cancelling2 = null;
74982
+ var CANCEL_TIMEOUT_MS2 = 5000;
74983
+ function setActiveRun2(run2) {
74984
+ activeRun2 = run2;
74985
+ }
74986
+ function trackStart(ws, projectId, started) {
74987
+ setActiveRun2(started.then((todoId) => ({ ws, projectId, todoId }), () => null));
74988
+ }
74989
+ function onShutdown(fn) {
74990
+ cleanups2.push(fn);
74991
+ }
74992
+ function withTimeout2(p, ms, fallback) {
74993
+ let timer;
74994
+ const timeout = new Promise((r) => {
74995
+ timer = setTimeout(() => r(fallback), ms);
74996
+ });
74997
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
74998
+ }
74999
+ function cancelActiveRun2(timeoutMs = CANCEL_TIMEOUT_MS2) {
75000
+ if (cancelling2)
75001
+ return cancelling2;
75002
+ const pending = activeRun2;
75003
+ if (!pending)
75004
+ return Promise.resolve({ ok: false, todoId: null });
75005
+ const res = { ok: false };
75006
+ const attempt = (async () => {
75007
+ const run2 = await pending;
75008
+ res.todoId = run2?.todoId ?? null;
75009
+ if (!run2)
75010
+ return res;
75011
+ if (!run2.ws.connected && !await run2.ws.connect())
75012
+ return res;
75013
+ res.ok = await run2.ws.sendInterrupt(run2.projectId, run2.todoId);
75014
+ return res;
75015
+ })().catch(() => res);
75016
+ cancelling2 = withTimeout2(attempt, timeoutMs, res).finally(() => {
75017
+ if (activeRun2 === pending)
75018
+ activeRun2 = null;
75019
+ cancelling2 = null;
75020
+ });
75021
+ return cancelling2;
75022
+ }
75023
+ function shutdown2(code, reason) {
75024
+ return exiting2 ??= (async () => {
75025
+ if (reason)
75026
+ process.stderr.write(`
75027
+ ${reason}
75028
+ `);
75029
+ if (activeRun2) {
75030
+ const { ok, todoId } = await cancelActiveRun2();
75031
+ if (ok)
75032
+ process.stderr.write(`Stopped todo ${todoId}
75033
+ `);
75034
+ else if (todoId)
75035
+ process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
75036
+ `);
75037
+ else if (todoId === undefined)
75038
+ process.stderr.write(`Warning: could not stop the todo being created (timed out)
75039
+ `);
75040
+ }
75041
+ for (const fn of cleanups2.splice(0)) {
75042
+ try {
75043
+ fn();
75044
+ } catch {}
75045
+ }
75046
+ process.exit(code);
75047
+ })();
75048
+ }
75049
+ function finish(code) {
75050
+ if (exiting2)
75051
+ return;
75052
+ process.exitCode = code;
75053
+ process.stdout.end(() => {
75054
+ if (!exiting2)
75055
+ process.exit(code);
75056
+ });
75057
+ }
75058
+ function installSignalHandlers() {
75059
+ process.on("SIGINT", () => void shutdown2(130, "Cancelled by user (Ctrl+C)"));
75060
+ process.on("SIGTERM", () => void shutdown2(143, "Terminated (SIGTERM)"));
75061
+ process.on("SIGHUP", () => void shutdown2(129, "Hangup (SIGHUP)"));
75062
+ }
75063
+
72901
75064
  // src/index.ts
72902
75065
  try {
72903
75066
  const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
@@ -72973,7 +75136,7 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72973
75136
  cfg.addToHistory(input2);
72974
75137
  process.stderr.write("\u2500".repeat(40) + `
72975
75138
  `);
72976
- await api2.addMessage(projectId, input2, agent2, todoId);
75139
+ await appendTracked(api2, ws, projectId, input2, agent2, todoId);
72977
75140
  await watchTodo(ws, todoId, projectId, {
72978
75141
  json: json2,
72979
75142
  autoApprove,
@@ -72984,6 +75147,11 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72984
75147
  }
72985
75148
  }
72986
75149
  }
75150
+ async function appendTracked(api2, ws, projectId, text, agent2, todoId) {
75151
+ const sent = api2.addMessage(projectId, text, agent2, todoId).then(() => todoId);
75152
+ trackStart(ws, projectId, sent);
75153
+ await sent;
75154
+ }
72987
75155
  async function linkToSpawningBlock(api2, todoId) {
72988
75156
  const { TODOFORAI_TODO_ID: pTodo, TODOFORAI_MESSAGE_ID: pMsg, TODOFORAI_BLOCK_ID: pBlock } = process.env;
72989
75157
  if (!pTodo || !pMsg || !pBlock || pTodo === todoId)
@@ -72992,12 +75160,7 @@ async function linkToSpawningBlock(api2, todoId) {
72992
75160
  `));
72993
75161
  }
72994
75162
  async function main() {
72995
- process.on("SIGINT", () => {
72996
- process.stderr.write(`
72997
- Cancelled by user (Ctrl+C)
72998
- `);
72999
- process.exit(130);
73000
- });
75163
+ installSignalHandlers();
73001
75164
  const { values: args, positionals } = parseCliArgs();
73002
75165
  const groupTag = args.group === undefined ? getEnv("GROUP_ID") : String(args.group);
73003
75166
  const groupName = args["group-name"] === undefined ? undefined : String(args["group-name"]);
@@ -73042,11 +75205,19 @@ Cancelled by user (Ctrl+C)
73042
75205
  printDeviceHelp();
73043
75206
  process.exit(0);
73044
75207
  }
75208
+ if (positionals[0] === "inbox" && args.help) {
75209
+ printInboxHelp();
75210
+ process.exit(0);
75211
+ }
75212
+ if (positionals[0] === "notify" && args.help) {
75213
+ printNotifyHelp();
75214
+ process.exit(0);
75215
+ }
73045
75216
  if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
73046
75217
  printListTodosHelp();
73047
75218
  process.exit(0);
73048
75219
  }
73049
- if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device"].includes(positionals[0])) {
75220
+ if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device", "inbox", "notify"].includes(positionals[0])) {
73050
75221
  printUsage();
73051
75222
  process.exit(0);
73052
75223
  }
@@ -73177,6 +75348,14 @@ Cancelled by user (Ctrl+C)
73177
75348
  await deviceCommand(api2, positionals, args);
73178
75349
  return;
73179
75350
  }
75351
+ if (positionals[0] === "inbox") {
75352
+ await inboxCommand(api2, positionals, args);
75353
+ return;
75354
+ }
75355
+ if (positionals[0] === "notify") {
75356
+ await notifyCommand(api2, positionals, args);
75357
+ return;
75358
+ }
73180
75359
  if (positionals[0] === "delete") {
73181
75360
  const todoId = positionals[1];
73182
75361
  if (!todoId) {
@@ -73533,6 +75712,7 @@ Cancelled by user (Ctrl+C)
73533
75712
  }
73534
75713
  if (!args["no-watch"]) {
73535
75714
  const ws2 = new FrontendWebSocket(apiUrl, apiKey);
75715
+ setActiveRun2({ ws: ws2, projectId: projectId2, todoId });
73536
75716
  await ws2.connect();
73537
75717
  const autoApprove = !!args["dangerously-skip-permissions"];
73538
75718
  let agent3 = todo2.agentSettings || { id: todo2.agentSettingsId };
@@ -73543,7 +75723,8 @@ Cancelled by user (Ctrl+C)
73543
75723
  await watchTodo(ws2, todoId, projectId2, {
73544
75724
  json: !!args.json,
73545
75725
  autoApprove,
73546
- agentSettings: agent3
75726
+ agentSettings: agent3,
75727
+ exitOnInterrupt: !!args["non-interactive"]
73547
75728
  });
73548
75729
  if (!args["non-interactive"]) {
73549
75730
  process.stderr.write(`
@@ -73592,9 +75773,9 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73592
75773
  const followUp = promptArg(positionals) ?? (process.stdin.isTTY ? "" : await readStdin());
73593
75774
  if (followUp) {
73594
75775
  cfg.addToHistory(followUp);
73595
- await api2.addMessage(projectId2, followUp, agent3, todoId);
75776
+ await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
73596
75777
  await linkToSpawningBlock(api2, todoId);
73597
- await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3 });
75778
+ await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3, exitOnInterrupt: !!args["non-interactive"] });
73598
75779
  }
73599
75780
  if (!args["non-interactive"]) {
73600
75781
  await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
@@ -73613,9 +75794,15 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73613
75794
  const bridgeReady = !args["no-bridge"] && !args["no-watch"] && !args.isolated ? ensureBridgeRunning2(apiUrl, apiKey) : null;
73614
75795
  const isolatedTodoId = args.isolated ? randomUUID3() : undefined;
73615
75796
  const mayfly = isolatedTodoId ? await spawnMayflyBridge(apiUrl, isolatedTodoId, realpathSync2(resolve4(args.path || ".")), { debug: !!args.debug, apiKey }) : null;
73616
- if (mayfly)
75797
+ if (mayfly) {
73617
75798
  process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
73618
75799
  `);
75800
+ onShutdown(mayfly.stop);
75801
+ mayfly.child.on("exit", (code, sig) => {
75802
+ if (!mayfly.stopped())
75803
+ shutdown2(1, `${RED3}Error: isolated bridge died (${sig ?? `code ${code}`}) \u2014 stopping todo${RESET2}`);
75804
+ });
75805
+ }
73619
75806
  let preMatchedAgent = null;
73620
75807
  let agents = null;
73621
75808
  if (args.agent) {
@@ -73719,15 +75906,16 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73719
75906
  agent2 = { ...agent2, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
73720
75907
  }
73721
75908
  cfg.addToHistory(content);
75909
+ const created = api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
75910
+ if (ws)
75911
+ trackStart(ws, projectId, created.then((t) => t.id));
73722
75912
  let todo;
73723
75913
  try {
73724
- todo = await api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
75914
+ todo = await created;
73725
75915
  } catch (e) {
73726
75916
  if (!args.project && !envProjectId && cfgScope.data.default_project_id === projectId && /failed: 403/.test(e.message || "")) {
73727
75917
  cfgScope.clearDefaultProject();
73728
- process.stderr.write(`${RED3}Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.${RESET2}
73729
- `);
73730
- process.exit(1);
75918
+ throw new Error(`Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.`);
73731
75919
  }
73732
75920
  throw e;
73733
75921
  }
@@ -73748,7 +75936,8 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73748
75936
  await watchTodo(ws, actualTodoId, projectId, {
73749
75937
  json: !!args.json,
73750
75938
  autoApprove,
73751
- agentSettings: agent2
75939
+ agentSettings: agent2,
75940
+ exitOnInterrupt: !!args["non-interactive"]
73752
75941
  });
73753
75942
  if (!args["non-interactive"]) {
73754
75943
  process.stderr.write(`
@@ -73760,10 +75949,6 @@ ${"\u2500".repeat(40)}
73760
75949
  }
73761
75950
  mayfly?.stop();
73762
75951
  }
73763
- var finish = (code) => {
73764
- process.exitCode = code;
73765
- process.stdout.end(() => process.exit(code));
73766
- };
73767
75952
  main().then(() => finish(process.exitCode ?? 0)).catch((e) => {
73768
75953
  process.stderr.write(`Error: ${e.message}
73769
75954
  `);