@todoforai/cli 0.1.66 → 0.1.68

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.
package/README.md CHANGED
@@ -39,6 +39,8 @@ echo "content" | tfa-cli # pipe from stdin
39
39
  tfa-cli --path /my/project "Fix bug" # explicit workspace
40
40
  ```
41
41
 
42
+ Ctrl+C (or closing the terminal) only exits the CLI — the todo keeps running; `tfa-cli --resume <id>` reattaches. Exception: `--isolated` runs are stopped, since they can't outlive their bridge.
43
+
42
44
  ### Start from a registry template
43
45
 
44
46
  ```bash
@@ -42969,6 +42969,9 @@ class ApiClient {
42969
42969
  unshareDevice(id, email) {
42970
42970
  return this.request("POST", `/api/v1/devices/${id}/unshare`, { id, email });
42971
42971
  }
42972
+ leaveDevice(id) {
42973
+ return this.request("POST", `/api/v1/devices/${id}/leave`, { id });
42974
+ }
42972
42975
  setDeviceWallpaperMode(id, mode) {
42973
42976
  return this.request("POST", `/api/v1/devices/${id}/wallpaper-mode`, { id, mode });
42974
42977
  }
@@ -43008,6 +43011,15 @@ class ApiClient {
43008
43011
  listProjectGroups(projectId) {
43009
43012
  return this.request("GET", `/api/v1/projects/${projectId}/groups`);
43010
43013
  }
43014
+ getProjectGroupIntake(projectId, groupTag) {
43015
+ return this.request("GET", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(groupTag)}/intake`);
43016
+ }
43017
+ updateProjectGroupIntake(projectId, groupTag, fields) {
43018
+ return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(groupTag)}/intake`, { projectId, groupTag, ...fields });
43019
+ }
43020
+ regenerateProjectGroupIntake(projectId, groupTag) {
43021
+ return this.request("POST", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(groupTag)}/intake/regenerate`, { projectId, groupTag });
43022
+ }
43011
43023
  upsertProjectGroup(projectId, slug, fields) {
43012
43024
  return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
43013
43025
  }
@@ -43609,6 +43621,7 @@ var TodoStatus;
43609
43621
  TodoStatus2["PAUSED"] = "PAUSED";
43610
43622
  TodoStatus2["POSTPONED"] = "POSTPONED";
43611
43623
  TodoStatus2["REVIEW_REQUESTED"] = "REVIEW_REQUESTED";
43624
+ TodoStatus2["WAITING_AGENT"] = "WAITING_AGENT";
43612
43625
  TodoStatus2["RUNNING"] = "RUNNING";
43613
43626
  TodoStatus2["WAITING_FOR_USER"] = "WAITING_FOR_USER";
43614
43627
  TodoStatus2["COMPACTING"] = "COMPACTING";
@@ -43623,6 +43636,11 @@ var TodoStatus;
43623
43636
  TodoStatus2["ARCHIVED"] = "ARCHIVED";
43624
43637
  TodoStatus2["DELETED"] = "DELETED";
43625
43638
  })(TodoStatus ||= {});
43639
+ var CLOSED_STATUSES = [
43640
+ "DONE" /* DONE */,
43641
+ "ARCHIVED" /* ARCHIVED */,
43642
+ "DELETED" /* DELETED */
43643
+ ];
43626
43644
  // ../packages/shared-fbe/src/constants.ts
43627
43645
  var APPEAL_EMAIL = "support@todofor.ai";
43628
43646
  var ACCOUNT_SUSPENDED_MESSAGE = `Account suspended for suspected abuse. If you think this is a mistake, email ${APPEAL_EMAIL}.`;
@@ -43639,22 +43657,28 @@ var PROVIDER_BY_AUTHOR = {
43639
43657
  "x-ai": "xai",
43640
43658
  "z-ai": "z.ai",
43641
43659
  google: "google-ai-studio",
43642
- moonshotai: "opencode_go"
43660
+ moonshotai: "opencode_go",
43661
+ meta: "meta"
43643
43662
  };
43644
43663
  var MODEL_PROVIDER_OVERRIDES = {
43645
43664
  "openai/gpt-5.5-pro": "openrouter"
43646
43665
  };
43666
+ var MODEL_ENDPOINT_OVERRIDES = {
43667
+ "google/gemini-4-argon": { "google-ai-studio": "vertex" }
43668
+ };
43647
43669
  function resolveDispatchProvider(modelId, endpointProvider) {
43648
- return MODEL_PROVIDER_OVERRIDES[modelId.toLowerCase()] ?? endpointProvider;
43670
+ const id = modelId.toLowerCase();
43671
+ return MODEL_PROVIDER_OVERRIDES[id] ?? MODEL_ENDPOINT_OVERRIDES[id]?.[endpointProvider.toLowerCase()] ?? endpointProvider;
43649
43672
  }
43650
43673
  var SUBSCRIPTION_PROVIDERS = new Set(["opencode_go"]);
43651
43674
  function applyProviderOverride(qualified) {
43652
43675
  const colon = qualified.indexOf(":");
43676
+ const provider = qualified.slice(0, colon);
43653
43677
  const rest = qualified.slice(colon + 1);
43654
43678
  const suffix = rest.match(/\([^)]+\)$/)?.[0] ?? "";
43655
43679
  const bare = (suffix ? rest.slice(0, -suffix.length) : rest).toLowerCase();
43656
- const override = MODEL_PROVIDER_OVERRIDES[bare];
43657
- return override ? `${override}:${bare}${suffix}` : qualified;
43680
+ const resolved = resolveDispatchProvider(bare, provider);
43681
+ return resolved === provider ? qualified : `${resolved}:${bare}${suffix}`;
43658
43682
  }
43659
43683
  function isQualifiedModel(model) {
43660
43684
  const id = model.trim();
@@ -43677,9 +43701,9 @@ function qualifyModel(model) {
43677
43701
  const override = MODEL_PROVIDER_OVERRIDES[lower];
43678
43702
  if (override)
43679
43703
  return { model: `${override}:${lower}${suffix}` };
43680
- const provider = PROVIDER_BY_AUTHOR[lower.split("/")[0]];
43681
- if (provider && lower.includes("/"))
43682
- return { model: `${provider}:${lower}${suffix}` };
43704
+ const author = PROVIDER_BY_AUTHOR[lower.split("/")[0]];
43705
+ if (author && lower.includes("/"))
43706
+ return { model: `${resolveDispatchProvider(lower, author)}:${lower}${suffix}` };
43683
43707
  return { model: id };
43684
43708
  }
43685
43709
  function qualifiedModelIds(listing, filter) {
@@ -43738,6 +43762,7 @@ var mimetypes_default = {
43738
43762
  js: { mime: "text/javascript", type: "text" },
43739
43763
  ts: { mime: "text/typescript", type: "text" },
43740
43764
  html: { mime: "text/html", type: "text" },
43765
+ htm: { mime: "text/html", type: "text" },
43741
43766
  css: { mime: "text/css", type: "text" },
43742
43767
  xml: { mime: "text/xml", type: "text" },
43743
43768
  py: { mime: "text/x-python", type: "text" },
@@ -43776,7 +43801,7 @@ var mimetypes_default = {
43776
43801
  docx: { mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", type: "document", tags: ["docx"] },
43777
43802
  ppt: { mime: "application/vnd.ms-powerpoint", type: "document" },
43778
43803
  pptx: { mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation", type: "document", tags: ["pptx"] },
43779
- xls: { mime: "application/vnd.ms-excel", type: "spreadsheet" },
43804
+ xls: { mime: "application/vnd.ms-excel", type: "spreadsheet", tags: ["xls"] },
43780
43805
  xlsx: { mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", type: "spreadsheet", tags: ["xlsx"] },
43781
43806
  zip: { mime: "application/zip", type: "archive" },
43782
43807
  rar: { mime: "application/vnd.rar", type: "archive" },
@@ -44169,6 +44194,8 @@ var USER_HEADINGS = new Set([
44169
44194
  "steps",
44170
44195
  "data needed"
44171
44196
  ]);
44197
+ // ../packages/shared-fbe/src/bridgeProtocol.ts
44198
+ var ADB_DATA_CHUNK = 40 * 1024;
44172
44199
  // ../packages/shared-fbe/src/bashPatterns.ts
44173
44200
  function splitShellCommands(input) {
44174
44201
  const parts = [];
@@ -44410,7 +44437,6 @@ function getBlockServerId(block) {
44410
44437
  const prefix = getBlockServerPrefix(block);
44411
44438
  return prefix ? prefix.slice(0, -1) : "*";
44412
44439
  }
44413
-
44414
44440
  // ../packages/shared-fbe/src/permissionUtils.ts
44415
44441
  function parsePattern(pattern) {
44416
44442
  const colonIndex = pattern.indexOf(":");
@@ -44640,7 +44666,7 @@ Exactly ONE \`api_recommend_template\` call per proposal: it carries both what t
44640
44666
  };
44641
44667
  var JARVIS_AGENT = {
44642
44668
  id: "00000000-0000-0000-0000-202601013152",
44643
- name: "JARVIS",
44669
+ name: "Tod",
44644
44670
  ownerId: "SYSTEM",
44645
44671
  systemMessage: "",
44646
44672
  model: "",
@@ -44967,6 +44993,7 @@ var tool_catalog_default = {
44967
44993
  label: "GitLab",
44968
44994
  summary: "Work with your GitLab repos, merge requests and pipelines.",
44969
44995
  statusCmd: "glab auth status 2>&1 | grep -o 'Logged in to [^ ]* [a-z]* [^ ]*' | paste -sd ' ' - | grep -m1 .",
44996
+ logoutCmd: "glab auth logout --hostname gitlab.com 2>&1 </dev/null",
44970
44997
  loginCmd: "glab auth login --device",
44971
44998
  credentialPaths: ["~/.config/glab-cli/config.yml"],
44972
44999
  description: "GitLab equivalent of `gh`.",
@@ -45128,6 +45155,7 @@ var tool_catalog_default = {
45128
45155
  credentialPaths: ["~/.config/shopify-cli-kit-nodejs/config.json"],
45129
45156
  versionCmd: "shopify version 2>/dev/null | tail -n 1",
45130
45157
  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`,
45158
+ logoutCmd: "shopify auth logout",
45131
45159
  deviceCodePattern: "verification code: ([A-Z0-9-]+)",
45132
45160
  oauthUrlPattern: "(https://accounts\\.shopify\\.com/activate-with-code\\S*)",
45133
45161
  canStreamLogin: true,
@@ -45173,7 +45201,7 @@ var tool_catalog_default = {
45173
45201
  installer: "npm",
45174
45202
  label: "TODOforAI",
45175
45203
  summary: "Create and follow up on your TODOs, and show results in the chat.",
45176
- 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, private to the todo\'s readers; `--card <name>` via project renderer; `--alias <name>` updates in place, `--alias <todoId>:<name>` updates another todo\'s artifact; `show share <ref>` makes it public and prints the link, `show unshare <ref>` makes it private again), `open <url>` embeds a live preview. Pick by where the output lives: read once → plain reply; interactive, this chat only → `html_snippet`; kept/revised later → `show --alias`, then `show share` only if someone outside needs a link; format the user asked for (pptx, docx…) → `show` the file. 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 (shown as from you); `--to <email>` reaches a member of the current project instead (`project members` lists them, 20/day) — nobody else; same text is delivered once, max 10/day to the user, 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.',
45204
+ 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, private to the todo\'s readers; `--card <name>` via project renderer; `--alias <name>` updates in place, `--alias <todoId>:<name>` updates another todo\'s artifact; `show share <ref>` makes it public and prints the link, `show unshare <ref>` makes it private again), `open <url>` embeds a live preview. Show viewer chrome: `--title` appears in the header; a kebab menu holds file actions, versions and sharing when available. Desktop floating previews add minimize (park in a tab), maximize/restore and close (X); inline blocks have no window controls. For authored HTML, do not duplicate the header/controls or add a rounded, bordered root card: the floating viewer owns the rounded frame. Keep content edge-to-edge; internal layout padding is fine. Pick by where the output lives: read once → plain reply; interactive, this chat only → `html_snippet`; kept/revised later → `show --alias`, then `show share` only if someone outside needs a link; format the user asked for (pptx, docx…) → `show` the file. 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 (shown as from you); `--to <email>` reaches a member of the current project instead (`project members` lists them, 20/day) — nobody else; same text is delivered once, max 10/day to the user, 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. Project files: `memory sync|path [--project <id>]` requires a project and checks out only projects/<id>/{memory,context} and projects/<id>/me/{memory,context}; legacy global files are read-only recovery. Voice commands use `--scope mine|project` (default mine, private within this project; project is shared).',
45177
45205
  installCmd: "npm install -g @todoforai/cli",
45178
45206
  versionCmd: "tfa-cli --version 2>/dev/null | sed -n 1p",
45179
45207
  preinstallCloud: true,
@@ -45321,6 +45349,7 @@ var tool_catalog_default = {
45321
45349
  summary: "Manage your Microsoft Azure resources.",
45322
45350
  binName: "az",
45323
45351
  statusCmd: "az account show --query user.name -o tsv 2>/dev/null | tr -d '\\r' | grep -m1 .",
45352
+ logoutCmd: "az logout 2>&1; az account clear 2>&1",
45324
45353
  loginCmd: "az login --use-device-code",
45325
45354
  oauthUrlPattern: "(https://login\\.microsoft\\.com/device\\S*)",
45326
45355
  deviceCodePattern: "enter the code ([A-Z0-9]+)",
@@ -45406,7 +45435,7 @@ var tool_catalog_default = {
45406
45435
  preinstallCloud: true,
45407
45436
  label: "Browser",
45408
45437
  summary: "Browse websites automatically: click, type, read and screenshot.",
45409
- 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`. Never `sleep`: `open` returns after load, but `click`/`fill` fail at once if the element isn't there yet — `wait '<sel>'`, `wait --text 'Done'`, `wait --url '**/dash'`, `wait --load networkidle` or `wait '#spinner' --state hidden` first.",
45438
+ 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`. Never `sleep`: `open` returns after load, but `click`/`fill` fail at once if the element isn't there yet — `wait '<sel>'`, `wait --text 'Done'`, `wait --url '**/dash'`, `wait --load networkidle` or `wait '#spinner' --state hidden` first. Extension browsers (WEB_EXTENSION) only: try sign-in with credentials from `tfa-vault`; request human help only for a step you cannot complete yourself (MFA/CAPTCHA/consent). Use Node/Bun WebSocket on the listed CDP URL; `Target.getTargets` or `device_list` gives `tab-N`, whose N is the numeric tabId. On the SAME socket send `{id: 1, method: 'TodoforAI.requestHumanHelp', params: {tabId: N, message: 'what to do'}}` (message 1–500 chars). This focuses and pauses the tab, sets its yellow `?` badge, and shows needs-you in the extension popup. Tell the user to click Resume control and reply here, then end your turn; Resume does not wake the agent. Afterwards reconnect and snapshot fresh. `TodoforAI.humanHelpStatus` with `{tabId}` remains available while paused. If bootstrap fails on a restricted target, direct CDP `Target.createTarget` with an ordinary URL opens your tab on the SAME socket; pass numeric N from returned `tab-N` as `tabId`.",
45410
45439
  versionCmd: "agent-browser --version 2>/dev/null | sed -n 1p",
45411
45440
  preinstallJarvis: true
45412
45441
  },
@@ -45548,6 +45577,17 @@ var tool_catalog_default = {
45548
45577
  preinstallCloud: true,
45549
45578
  preinstallJarvis: true
45550
45579
  },
45580
+ "tfa-find": {
45581
+ internal: true,
45582
+ category: "todoforai",
45583
+ pkg: "@todoforai/tfa-find",
45584
+ installer: "npm",
45585
+ label: "File Search",
45586
+ summary: "Find documents, photos and more by describing what you need.",
45587
+ description: "Search files in folders you choose on your machine. Register folders, build a search index, then find relevant documents, images, audio and video with natural-language queries. CLI: tfa-find.",
45588
+ versionCmd: "tfa-find --version 2>/dev/null | sed -n 1p",
45589
+ installCmd: "npm install -g @todoforai/tfa-find"
45590
+ },
45551
45591
  "tfa-memory": {
45552
45592
  category: "todoforai",
45553
45593
  pkg: "@todoforai/tfa-memory",
@@ -45935,6 +45975,7 @@ var tool_catalog_default = {
45935
45975
  loginCmd: "mailchimp-api auth --api-key <api_key>",
45936
45976
  oauthBroker: true,
45937
45977
  statusCmd: `mailchimp-api whoami 2>/dev/null | sed -n 's/.*"account_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m1 .`,
45978
+ logoutCmd: "rm -f ~/.config/mailchimp-api/credentials.json",
45938
45979
  credentialPaths: [
45939
45980
  "~/.config/mailchimp-api/credentials.json"
45940
45981
  ],
@@ -45952,6 +45993,7 @@ var tool_catalog_default = {
45952
45993
  statusCmd: `facebook-pages-api whoami 2>/dev/null | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45953
45994
  loginCmd: "facebook-pages-api auth --page-id <id> --page-token <token>",
45954
45995
  oauthBroker: true,
45996
+ logoutCmd: "rm -f ~/.config/facebook-pages-api/credentials.json",
45955
45997
  credentialPaths: ["~/.config/facebook-pages-api/credentials.json"],
45956
45998
  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). Instagram (linked professional account, same page token): `ig whoami|media|get|stories|tags|limit`, `ig publish --image_url U | --video_url U [--media_type STORIES] | --carousel U1,U2 [--caption …]` (public URLs; container → wait → publish; `--create-only` = dry run; 100 posts/24h), `ig comments list|replies|create|reply|hide|delete|toggle`, `ig insights [media_id]` (needs instagram_manage_insights). `--help` for subcommands.",
45957
45999
  versionCmd: "facebook-pages-api --version 2>/dev/null | sed -n 1p"
@@ -45993,6 +46035,7 @@ var tool_catalog_default = {
45993
46035
  voiceChannel: "Outlook",
45994
46036
  summary: "Read and write your Outlook mail.",
45995
46037
  statusCmd: `outlook-api whoami 2>/dev/null | sed -n 's/.*"userPrincipalName"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
46038
+ logoutCmd: "outlook-api logout",
45996
46039
  loginCmd: "outlook-api auth",
45997
46040
  credentialPaths: ["~/.config/outlook-api/config.json"],
45998
46041
  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.",
@@ -46025,6 +46068,7 @@ var tool_catalog_default = {
46025
46068
  label: "Microsoft 365",
46026
46069
  summary: "Work with Microsoft 365 mail, Teams, SharePoint and users.",
46027
46070
  statusCmd: `m365 status --output text 2>/dev/null | tr -d '"' | grep -vi 'logged out' | grep -m1 .`,
46071
+ logoutCmd: "m365 logout",
46028
46072
  loginCmd: "m365 login --authType deviceCode --appId 14d82eec-204b-4c2f-b7e8-296a70dab67e",
46029
46073
  deviceCodePattern: "enter the code ([A-Z0-9]{8,}) to authenticate",
46030
46074
  oauthUrlPattern: "open the page (https://\\S+) and enter",
@@ -49944,21 +49988,67 @@ var tool_catalog_default = {
49944
49988
  },
49945
49989
  statusCmd: `zoo user view --format json 2>/dev/null | sed -n 's/.*"email": *"\\([^"]*\\)".*/\\1/p' | grep -m1 .`,
49946
49990
  loginCmd: "zoo auth login --web",
49991
+ logoutCmd: "printf 'y\\n' | zoo --host api.zoo.dev auth logout",
49947
49992
  loginNeedsPty: true,
49948
49993
  deviceCodePattern: "user code: ([A-Z0-9]{4}-[A-Z0-9]{4})",
49949
- oauthUrlPattern: "(https://api\\.zoo\\.dev/oauth2/device/verify\\S*)",
49994
+ oauthUrlPattern: "(https://api\\.zoo\\.dev/oauth2/device/verify\\?[^\\s']+)",
49950
49995
  canStreamLogin: true,
49951
49996
  credentialPaths: [
49952
49997
  "~/.config/zoo/hosts.toml"
49953
49998
  ],
49954
49999
  description: 'Official Zoo (KittyCAD) CLI. Text-to-CAD: `zoo ml text-to-cad export --output-format step "a 40mm gear"`; convert: `zoo file convert in.obj out/ --output-format step`; mass/volume: `zoo file mass|volume <file>`; KCL: `zoo kcl export|snapshot`; raw: `zoo api /user`. Token alternative: `echo <token> | zoo auth login --with-token`.',
49955
50000
  versionCmd: "zoo --version 2>/dev/null | sed -n 1p"
50001
+ },
50002
+ "python-docx": {
50003
+ category: "documents",
50004
+ pkg: "python-docx",
50005
+ pipModules: ["docx"],
50006
+ installer: "pip",
50007
+ label: "Word documents",
50008
+ summary: "Create, read and edit Word documents.",
50009
+ versionCmd: `{ "$HOME/.todoforai/tools/venv/bin/python" -c 'import docx; print(docx.__version__)' || python -c 'import docx; print(docx.__version__)' || python3 -c 'import docx; print(docx.__version__)' || py -c 'import docx; print(docx.__version__)'; } 2>/dev/null`,
50010
+ description: "Python python-docx library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
50011
+ },
50012
+ openpyxl: {
50013
+ category: "documents",
50014
+ pkg: "openpyxl",
50015
+ pipModules: ["openpyxl"],
50016
+ installer: "pip",
50017
+ label: "Excel workbooks",
50018
+ summary: "Create, read and edit Excel workbooks.",
50019
+ versionCmd: `{ "$HOME/.todoforai/tools/venv/bin/python" -c 'import openpyxl; print(openpyxl.__version__)' || python -c 'import openpyxl; print(openpyxl.__version__)' || python3 -c 'import openpyxl; print(openpyxl.__version__)' || py -c 'import openpyxl; print(openpyxl.__version__)'; } 2>/dev/null`,
50020
+ description: "Python openpyxl library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
50021
+ },
50022
+ "python-pptx": {
50023
+ category: "documents",
50024
+ pkg: "python-pptx",
50025
+ pipModules: ["pptx"],
50026
+ installer: "pip",
50027
+ label: "PowerPoint decks",
50028
+ summary: "Create, read and edit PowerPoint decks.",
50029
+ versionCmd: `{ "$HOME/.todoforai/tools/venv/bin/python" -c 'import pptx; print(pptx.__version__)' || python -c 'import pptx; print(pptx.__version__)' || python3 -c 'import pptx; print(pptx.__version__)' || py -c 'import pptx; print(pptx.__version__)'; } 2>/dev/null`,
50030
+ description: "Python python-pptx library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
50031
+ },
50032
+ "modal-h3": {
50033
+ category: "creative",
50034
+ pkg: "@todoforai/modal-h3",
50035
+ installer: "npm",
50036
+ label: "MiniMax-H3 on Modal",
50037
+ summary: "Generate videos with sound using MiniMax-H3 on your own Modal GPUs.",
50038
+ versionCmd: "modal-h3 --version 2>/dev/null | sed -n 1p",
50039
+ statusCmd: `modal-h3 whoami 2>/dev/null | grep '"authenticated": true' >/dev/null && echo authenticated`,
50040
+ loginCmd: "printf '%s' <proxy_token> | modal-h3 auth --token-stdin --fl2va-url <fl2va_url> --ref2va-url <ref2va_url>",
50041
+ credentialPaths: ["~/.config/modal-h3/credentials.json"],
50042
+ description: "MiniMax-H3 text/image/reference-to-video+audio on a self-deployed Modal app (SGLang, 4x H200, scale-to-zero). `modal-h3 generate -p '…' [--first @a.png] [--last …] [--ref-image|--ref-video|--ref-audio <url|@file>] [--duration 4-15] [--ratio 16:9|auto] [--quality high] -o out.mp4` (task inferred, retries 503 during cold start, polls, downloads), `warmup`, `status|download|delete <id>`, `list`, `api <METHOD> <path>`. Deploy once with `modal run modal/serve.py::download` + `modal deploy modal/serve.py`. Costs ~$18/h of 4x H200 while a container is up (cold start minutes, ~75 s per 5 s 768p clip warm); every request wakes a container, `whoami` is offline. Jobs live in container memory. 2K needs `minimax-api regenerate`."
49956
50043
  }
49957
50044
  };
49958
50045
 
49959
50046
  // ../packages/shared-fbe/src/toolCatalog.ts
49960
50047
  var TOOL_CATALOG = tool_catalog_default;
49961
50048
  // ../packages/shared-fbe/src/toolInstallCommand.ts
50049
+ var NPM_HOME = '"${TFA_NPM_HOME:-$HOME}"';
50050
+ var NPM_TOOLS = NPM_HOME.replace('}"', '}/.todoforai/tools"');
50051
+ var NPM_LOCAL = NPM_HOME.replace('}"', '}/.local"');
49962
50052
  var KEY_OF = new Map(Object.entries(TOOL_CATALOG).map(([k, e]) => [e, k]));
49963
50053
  var NODE_MIN_MAJOR = 24;
49964
50054
  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`;
@@ -50200,7 +50290,7 @@ var package_default = {
50200
50290
  type: "git",
50201
50291
  url: "git+https://github.com/todoforai/cli.git"
50202
50292
  },
50203
- version: "0.1.66",
50293
+ version: "0.1.68",
50204
50294
  type: "module",
50205
50295
  bin: {
50206
50296
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -50330,6 +50420,13 @@ Usage:
50330
50420
 
50331
50421
  todo-id defaults to $TODOFORAI_TODO_ID (agent shell) or the last todo this CLI touched.
50332
50422
 
50423
+ Viewer chrome:
50424
+ --title appears in the header; a kebab menu holds file actions, versions and sharing when available.
50425
+ Desktop floating previews add minimize (park in a tab), maximize/restore and close (X).
50426
+ Inline blocks have no window controls. Do not draw these controls or a duplicate title bar in authored HTML.
50427
+ The floating viewer owns the rounded frame. Keep the HTML root flat, without a border, radius, shadow or outer margin;
50428
+ internal content padding and spacing are fine. Existing files and websites keep their own styling.
50429
+
50333
50430
  When to use what:
50334
50431
  read once, move on → plain chat reply
50335
50432
  interactive, this chat only → html_snippet (can exec/connect/chat back)
@@ -51404,15 +51501,20 @@ function blockToAnthropic(block, mode, format) {
51404
51501
  const msg = block.error_message || block.content || "(unknown error)";
51405
51502
  return [{ type: "text", text: `[error] ${msg}` }];
51406
51503
  }
51504
+ const name = t === "named_tool" ? block.tool_name === "run_shell" ? "bash" : block.tool_name : t;
51407
51505
  const inputFields = [];
51408
51506
  for (const [k, v] of Object.entries(block)) {
51409
- if (BLOCK_META_KEYS.has(k))
51507
+ if (BLOCK_META_KEYS.has(k) || k === "args" || t === "named_tool" && k === "tool_name")
51410
51508
  continue;
51411
51509
  if (v === undefined || v === null || v === "")
51412
51510
  continue;
51413
51511
  inputFields.push([k, v]);
51414
51512
  }
51415
- const use = format === "anthropic" ? { type: "tool_use", id: block.id, name: t, input: Object.fromEntries(inputFields) } : { type: "tool_use", content: `<${t}${inputFields.map(([k, v]) => ` ${k}=${xmlAttr(v)}`).join("")}/>` };
51513
+ for (const [k, v] of Object.entries(block.args ?? {})) {
51514
+ if (!inputFields.some(([key]) => key === k))
51515
+ inputFields.push([k, v]);
51516
+ }
51517
+ const use = format === "anthropic" ? { type: "tool_use", id: block.id, name, input: Object.fromEntries(inputFields) } : { type: "tool_use", content: `<${name}${inputFields.map(([k, v]) => ` ${k}=${xmlAttr(v)}`).join("")}/>` };
51416
51518
  if (format === "compact" && mode !== "default")
51417
51519
  use.id = block.id;
51418
51520
  const items = [use];
@@ -51428,8 +51530,9 @@ function blockToAnthropic(block, mode, format) {
51428
51530
  source.name = att.originalName;
51429
51531
  parts.push({ type: isImage ? "image" : "document", source });
51430
51532
  }
51431
- if (typeof r.content === "string" && r.content)
51432
- parts.push({ type: "text", text: r.content });
51533
+ const text = r.content || r.meta?.text;
51534
+ if (typeof text === "string" && text)
51535
+ parts.push({ type: "text", text });
51433
51536
  return parts;
51434
51537
  });
51435
51538
  const result = { type: "tool_result", tool_use_id: block.id };
@@ -51446,8 +51549,9 @@ function blockToAnthropic(block, mode, format) {
51446
51549
  const nameAttr = att.originalName && !isAutoName ? ` name=${xmlAttr(att.originalName)}` : "";
51447
51550
  parts.push(`<attachment uri=${xmlAttr(att.uri)}${nameAttr} size=${att.fileSize}/>`);
51448
51551
  }
51449
- if (typeof r.content === "string" && r.content)
51450
- parts.push(r.content);
51552
+ const text = r.content || r.meta?.text;
51553
+ if (typeof text === "string" && text)
51554
+ parts.push(text);
51451
51555
  }
51452
51556
  const result = { type: "tool_result", content: parts.join(`
51453
51557
  `) };
@@ -52673,6 +52777,7 @@ function renderDiff(originalContent, modifiedContent, filePath) {
52673
52777
 
52674
52778
  // src/shutdown.ts
52675
52779
  var activeRun = null;
52780
+ var stopOnExit = false;
52676
52781
  var cleanups = [];
52677
52782
  var exiting = null;
52678
52783
  var cancelling = null;
@@ -52680,6 +52785,9 @@ var CANCEL_TIMEOUT_MS = 5000;
52680
52785
  function setActiveRun(run) {
52681
52786
  activeRun = run;
52682
52787
  }
52788
+ var stopsRunOnExit = () => stopOnExit;
52789
+ var detachNotice = (todoId) => `Todo keeps running — reattach: tfa-cli --resume ${todoId}
52790
+ `;
52683
52791
  function withTimeout(p, ms, fallback) {
52684
52792
  let timer;
52685
52793
  const timeout = new Promise((r) => {
@@ -52691,7 +52799,7 @@ function cancelActiveRun(timeoutMs = CANCEL_TIMEOUT_MS) {
52691
52799
  if (cancelling)
52692
52800
  return cancelling;
52693
52801
  const pending = activeRun;
52694
- if (!pending)
52802
+ if (!pending || !stopOnExit)
52695
52803
  return Promise.resolve({ ok: false, todoId: null });
52696
52804
  const res = { ok: false };
52697
52805
  const attempt = (async () => {
@@ -52717,7 +52825,11 @@ function shutdown(code, reason) {
52717
52825
  process.stderr.write(`
52718
52826
  ${reason}
52719
52827
  `);
52720
- if (activeRun) {
52828
+ if (activeRun && !stopOnExit) {
52829
+ const run = await withTimeout(Promise.resolve(activeRun), CANCEL_TIMEOUT_MS, null);
52830
+ if (run)
52831
+ process.stderr.write(detachNotice(run.todoId));
52832
+ } else if (activeRun) {
52721
52833
  const { ok, todoId } = await cancelActiveRun();
52722
52834
  if (ok)
52723
52835
  process.stderr.write(`Stopped todo ${todoId}
@@ -52829,20 +52941,6 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
52829
52941
  diffStoreByWs.set(ws, diffStore);
52830
52942
  const diffRendered = new Set;
52831
52943
  let approveAll = !!opts.autoApprove;
52832
- let interruptCount = 0;
52833
- const origHandler = process.listeners("SIGINT").slice();
52834
- process.removeAllListeners("SIGINT");
52835
- process.on("SIGINT", () => {
52836
- interruptCount++;
52837
- if (opts.exitOnInterrupt || interruptCount >= 2)
52838
- return void shutdown(130, "Cancelled by user (Ctrl+C)");
52839
- process.stderr.write(`
52840
- ${YELLOW}Interrupting... (Ctrl+C again to force exit)${RESET}
52841
- `);
52842
- if (opts.interruptOnCancel !== false) {
52843
- ws.sendInterrupt(projectId, todoId);
52844
- }
52845
- });
52846
52944
  const pendingBlocks = [];
52847
52945
  let approvalPromptActive = false;
52848
52946
  let activeApprovalBlocks = [];
@@ -53065,14 +53163,10 @@ ${YELLOW}Connection lost — reconnecting...${RESET}
53065
53163
  await cancelActiveRun();
53066
53164
  process.exitCode = 1;
53067
53165
  if (!opts.suppressCancelNotice) {
53068
- process.stderr.write(`${YELLOW}Interrupted${RESET}
53069
- `);
53166
+ process.stderr.write(stopsRunOnExit() ? `${YELLOW}Interrupted${RESET}
53167
+ ` : detachNotice(todoId));
53070
53168
  }
53071
53169
  return false;
53072
- } finally {
53073
- process.removeAllListeners("SIGINT");
53074
- for (const fn of origHandler)
53075
- process.on("SIGINT", fn);
53076
53170
  }
53077
53171
  }
53078
53172
 
@@ -53641,6 +53735,8 @@ Usage:
53641
53735
  tfa-cli project settings <field=value>… Project settings (PUT /settings/projects/{id})
53642
53736
  tfa-cli project groups List todo groups
53643
53737
  tfa-cli project groups set <slug> <field=value>… Create/update a group: name description pinned archived order
53738
+ tfa-cli project groups intake <slug> [on|off|regenerate] [field=value]…
53739
+ Public intake: autoStart maxSubmissions postIpLimit placeholder agentSettingsId
53644
53740
  tfa-cli project groups reorder <slug>… Set wall order
53645
53741
  tfa-cli project surfaces List the board's card surfaces (live refs shown as cards)
53646
53742
  tfa-cli project surfaces set <id> ref=<ref> [title=…] [group=<slug>] [order=N]
@@ -53805,6 +53901,25 @@ async function projectCommand(api, positionals, args, projectId) {
53805
53901
  `);
53806
53902
  return;
53807
53903
  }
53904
+ if (verb === "intake") {
53905
+ const [slug, ...options] = gargs;
53906
+ if (!slug)
53907
+ fail("Usage: tfa-cli project groups intake <slug> [on|off|regenerate] [field=value]…");
53908
+ const action = ["on", "off", "regenerate"].includes(options[0]) ? options.shift() : undefined;
53909
+ if (options.length && action === "regenerate")
53910
+ fail("regenerate does not accept settings");
53911
+ const fields = parseAssignments2(options);
53912
+ const intake = action === "regenerate" ? await api.regenerateProjectGroupIntake(projectId, slug) : action || options.length ? await api.updateProjectGroupIntake(projectId, slug, { ...fields, ...action && { enabled: action === "on" } }) : await api.getProjectGroupIntake(projectId, slug);
53913
+ if (args.json) {
53914
+ console.log(JSON.stringify(intake, null, 2));
53915
+ return;
53916
+ }
53917
+ process.stderr.write(`${slug}: public ${intake.enabled ? "on" : "off"}, ${intake.submissionCount}/${intake.maxSubmissions}, autoStart=${intake.autoStart}
53918
+ `);
53919
+ if (intake.url)
53920
+ console.log(intake.url);
53921
+ return;
53922
+ }
53808
53923
  if (verb === "reorder") {
53809
53924
  if (!gargs.length)
53810
53925
  fail("Usage: tfa-cli project groups reorder <slug>…");
@@ -53880,11 +53995,10 @@ Usage:
53880
53995
  tfa-cli brand voice correct "<what is off>" Tell the learner what it got wrong; profile is updated
53881
53996
  in one pass and the correction is kept for every re-learn
53882
53997
 
53883
- Every voice verb takes --scope project|mine|personal — which voice to teach:
53884
- project the project's, shared with its members (default; needs write access)
53885
- mine yours, only in this project
53886
- personal yours, in every project (e.g. your own Gmail)
53887
- Agents read all three; on the same voice name the narrower wins: mine > project > personal.
53998
+ Every voice verb takes --scope mine|project — which voice to teach:
53999
+ mine yours, only in this project (default)
54000
+ project the project's, shared with its members (needs write access)
54001
+ On the same voice name the narrower wins: mine > project.
53888
54002
 
53889
54003
 
53890
54004
  Questions the UI asks (keys for 'answers'):
@@ -53984,9 +54098,9 @@ async function voiceDeviceCommand(rest, args) {
53984
54098
  }
53985
54099
  async function voiceCommand(api, projectId, rest, args) {
53986
54100
  const [verb, ...vargs] = rest;
53987
- const scope = args.scope;
53988
- if (scope && !["project", "mine", "personal"].includes(scope))
53989
- fail(`--scope must be project, mine or personal, got '${scope}'`);
54101
+ const scope = args.scope ?? "mine";
54102
+ if (!["project", "mine"].includes(scope))
54103
+ fail(`--scope must be mine or project, got '${scope}'`);
53990
54104
  if (verb === "collect" && !vargs[0])
53991
54105
  fail("Usage: tfa-cli brand voice collect <channel> [--url U | --account <page-id> | --max N] [--dry-run]");
53992
54106
  if (verb === "answers") {
@@ -54113,6 +54227,7 @@ Usage:
54113
54227
  tfa-cli device access <device> who you shared the device with
54114
54228
  tfa-cli device share <device> <email> let a teammate use it (their runs act as THEM)
54115
54229
  tfa-cli device unshare <device> <email>
54230
+ tfa-cli device leave <device> remove your own access to a shared device
54116
54231
 
54117
54232
  <device> is an id, name or hostname (unique partial works).
54118
54233
  Devices shared with you are marked "shared"; only the owner can rename/share them.
@@ -54149,6 +54264,12 @@ async function deviceCommand(api, positionals, args) {
54149
54264
  fail("Usage: tfa-cli device rename <device> <name>");
54150
54265
  await api.renameDevice(found.id, value);
54151
54266
  process.stderr.write(`${GREEN}✅ ${found.name} → ${value}${RESET}
54267
+ `);
54268
+ return;
54269
+ }
54270
+ if (sub === "leave") {
54271
+ await api.leaveDevice(found.id);
54272
+ process.stderr.write(`${GREEN}✅ Removed ${found.name} from your devices${RESET}
54152
54273
  `);
54153
54274
  return;
54154
54275
  }
@@ -54198,15 +54319,7 @@ var STATUS_COLOR = {
54198
54319
  POSTPONED: CYAN,
54199
54320
  PAUSED: CYAN
54200
54321
  };
54201
- var CLOSED = new Set([
54202
- "DONE",
54203
- "READY_CHECKED",
54204
- "CANCELLED",
54205
- "CANCELLED_CHECKED",
54206
- "ERROR_CHECKED",
54207
- "ARCHIVED",
54208
- "DELETED"
54209
- ]);
54322
+ var CLOSED = new Set(CLOSED_STATUSES);
54210
54323
  var VALID_STATUS = new Set(Object.values(TodoStatus));
54211
54324
  var isOpen = (s) => !CLOSED.has(s);
54212
54325
  function printListTodosHelp() {
@@ -54218,11 +54331,16 @@ Usage:
54218
54331
 
54219
54332
  Flags:
54220
54333
  -n, --limit <n> Max rows to show (default: 30)
54221
- --cursor <n> Fetch todos older than this cursor (lastActivityAt)
54222
- --page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
54334
+ --cursor <n> Fetch todos older than this cursor (sort field, epoch ms)
54335
+ --since <t> Only todos with sort field >= t (inclusive)
54336
+ --until <t> Only todos with sort field < t (exclusive)
54337
+ t = epoch ms | YYYY-MM-DD (local midnight) | ISO datetime | 12h / 3d ago
54338
+ --sort <key> activity (lastActivityAt, default) | created (createdAt)
54339
+ --contributor <u> Only todos a member opened/wrote in (id or email; comma = any)
54340
+ --page-size <n> Backend page size per request (default: min(limit, 400), max: 400)
54223
54341
  --search <text> Search content/status on the backend
54224
54342
  -s, --status <S[,S2]> Filter by status (comma-separated, union).
54225
- -A, --all Include DONE (also CANCELLED, ARCHIVED, …)
54343
+ -A, --all Include closed (DONE, ARCHIVED, DELETED)
54226
54344
  --project <id> Project ID (default: current default project)
54227
54345
  --json Output { items, nextCursor } as JSON
54228
54346
  -h, --help Show this help
@@ -54233,6 +54351,8 @@ Examples:
54233
54351
  todoforai-cli list --cursor 1719234567890 # next page
54234
54352
  todoforai-cli list --all # include DONE
54235
54353
  todoforai-cli list -s RUNNING,REVIEW_REQUESTED
54354
+ todoforai-cli list -A --since 2026-10-11 --until 2026-10-12 # touched that day
54355
+ todoforai-cli list -A --sort created --since 7d --contributor a@b.com
54236
54356
  todoforai-cli list --search bug --json | jq '.items[].id'
54237
54357
  `);
54238
54358
  }
@@ -54258,6 +54378,25 @@ function cursorValue(value) {
54258
54378
  }
54259
54379
  return n;
54260
54380
  }
54381
+ function parseTime(value, name, now = Date.now()) {
54382
+ if (value === undefined)
54383
+ return;
54384
+ const s = String(value).trim();
54385
+ const rel = s.match(/^(\d+)([mhd])$/);
54386
+ let n;
54387
+ if (/^\d+$/.test(s))
54388
+ n = Number(s);
54389
+ else if (rel)
54390
+ n = now - Number(rel[1]) * { m: 60000, h: 3600000, d: 86400000 }[rel[2]];
54391
+ else
54392
+ n = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00` : s);
54393
+ if (!Number.isFinite(n) || n < 0) {
54394
+ process.stderr.write(`${RED}Error: ${name} must be epoch ms, YYYY-MM-DD, ISO datetime or e.g. 12h / 3d${RESET}
54395
+ `);
54396
+ process.exit(2);
54397
+ }
54398
+ return n;
54399
+ }
54261
54400
  function toItemsPage(response) {
54262
54401
  if (Array.isArray(response))
54263
54402
  return { items: response };
@@ -54266,8 +54405,25 @@ function toItemsPage(response) {
54266
54405
  nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
54267
54406
  };
54268
54407
  }
54269
- function rowTime(t) {
54270
- return Number(t.lastActivityAt ?? t.createdAt ?? 0);
54408
+ function rowTime(t, sort = "activity") {
54409
+ return Number((sort === "created" ? t.createdAt : t.lastActivityAt ?? t.createdAt) ?? 0);
54410
+ }
54411
+ async function resolveContributors(api, projectId, raw) {
54412
+ const wanted = raw.split(",").map((s) => s.trim()).filter(Boolean);
54413
+ if (!wanted.some((w) => w.includes("@")))
54414
+ return wanted;
54415
+ const members = await api.getProjectMembers(projectId);
54416
+ return wanted.map((w) => {
54417
+ if (!w.includes("@"))
54418
+ return w;
54419
+ const m = members.find((m2) => m2.email?.toLowerCase() === w.toLowerCase());
54420
+ if (!m) {
54421
+ process.stderr.write(`${RED}Error: no project member with email ${w}${RESET}
54422
+ `);
54423
+ process.exit(2);
54424
+ }
54425
+ return m.id;
54426
+ });
54271
54427
  }
54272
54428
  function printStatusError(unknown) {
54273
54429
  process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
@@ -54290,6 +54446,10 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54290
54446
  cursor: { type: "string" },
54291
54447
  "page-size": { type: "string" },
54292
54448
  search: { type: "string" },
54449
+ since: { type: "string" },
54450
+ until: { type: "string" },
54451
+ sort: { type: "string" },
54452
+ contributor: { type: "string" },
54293
54453
  status: { type: "string", short: "s" },
54294
54454
  all: { type: "boolean", short: "A", default: false },
54295
54455
  project: { type: "string" },
@@ -54309,9 +54469,20 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54309
54469
  process.exit(1);
54310
54470
  }
54311
54471
  const limit = positiveInt(values.limit, "--limit") ?? 30;
54312
- const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
54472
+ const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 400), 400);
54473
+ const sort = values.sort === undefined ? "activity" : String(values.sort);
54474
+ if (sort !== "activity" && sort !== "created") {
54475
+ process.stderr.write(`${RED}Error: --sort must be activity or created${RESET}
54476
+ `);
54477
+ process.exit(2);
54478
+ }
54479
+ const since = parseTime(values.since, "--since");
54480
+ const until = parseTime(values.until, "--until");
54313
54481
  let cursor = cursorValue(values.cursor);
54482
+ if (until !== undefined)
54483
+ cursor = cursor === undefined ? until : Math.min(cursor, until);
54314
54484
  const search = values.search ? String(values.search) : undefined;
54485
+ const contributorIds = values.contributor ? await resolveContributors(api, projectId, String(values.contributor)) : undefined;
54315
54486
  const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
54316
54487
  const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
54317
54488
  if (unknown.length)
@@ -54323,9 +54494,24 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54323
54494
  let backendNextCursor;
54324
54495
  while (rows.length < limit) {
54325
54496
  const requestLimit = Math.min(pageSize, limit - rows.length);
54326
- const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
54497
+ const page = toItemsPage(await api.listTodos(projectId, {
54498
+ limit: requestLimit,
54499
+ cursor,
54500
+ search,
54501
+ since,
54502
+ contributorIds,
54503
+ ...!includeClosed && { excludeClosed: true },
54504
+ ...sort !== "activity" && { sort }
54505
+ }));
54327
54506
  backendNextCursor = page.nextCursor;
54507
+ let exhausted = false;
54328
54508
  for (const t of page.items) {
54509
+ if (since !== undefined && rowTime(t) < since) {
54510
+ exhausted = true;
54511
+ break;
54512
+ }
54513
+ if (since !== undefined && rowTime(t, sort) < since)
54514
+ continue;
54329
54515
  const status = String(t.status).toUpperCase();
54330
54516
  if (!includeClosed && CLOSED.has(status))
54331
54517
  continue;
@@ -54335,14 +54521,18 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54335
54521
  if (rows.length >= limit)
54336
54522
  break;
54337
54523
  }
54524
+ if (exhausted) {
54525
+ backendNextCursor = undefined;
54526
+ break;
54527
+ }
54338
54528
  if (!backendNextCursor || page.items.length === 0)
54339
54529
  break;
54340
54530
  cursor = backendNextCursor;
54341
54531
  }
54342
- rows.sort((a, b) => rowTime(b) - rowTime(a));
54532
+ rows.sort((a, b) => rowTime(b, sort) - rowTime(a, sort));
54343
54533
  const visible = rows.slice(0, limit);
54344
54534
  const hasBufferedRows = rows.length > visible.length;
54345
- const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
54535
+ const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1], sort) : undefined;
54346
54536
  if (values.json) {
54347
54537
  process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
54348
54538
  `);
@@ -54360,7 +54550,7 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54360
54550
  for (const t of visible) {
54361
54551
  const status = String(t.status).toUpperCase();
54362
54552
  const sc = STATUS_COLOR[status] || DIM;
54363
- const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
54553
+ const ts = new Date(rowTime(t, sort)).toISOString().slice(0, 16).replace("T", " ");
54364
54554
  const title = String(t.content ?? "").split(`
54365
54555
  `)[0].slice(0, 100);
54366
54556
  process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
@@ -55248,7 +55438,6 @@ tfa-cli memory — memory and context folders as git checkouts
55248
55438
 
55249
55439
  Usage:
55250
55440
  tfa-cli memory sync [--project <id>] Clone, or pull, into:
55251
- ~/.todoforai/{memory,context} yours (todoforai:memory, todoforai:context)
55252
55441
  ~/.todoforai/projects/<id>/{memory,context} the project's (todoforai:projects/<id>/…)
55253
55442
  ~/.todoforai/projects/<id>/me/{memory,context} yours in it (todoforai:projects/<id>/me/…)
55254
55443
  tfa-cli memory path [--project <id>] Print the checkout paths
@@ -55258,14 +55447,17 @@ use plain git: log, diff, show, revert, commit, then \`git push\` — the push u
55258
55447
  folder. A push behind the server is refused: \`git pull\` first. Files over 5 MB live outside
55259
55448
  history (upload them instead). Project folders: members with write access push; your own in
55260
55449
  a project (me/) is private to you. The project
55261
- defaults to $TODOFORAI_PROJECT_ID.
55450
+ defaults to $TODOFORAI_PROJECT_ID and is required. Legacy global folders are read-only
55451
+ recovery, not synced.
55262
55452
  `);
55263
55453
  }
55264
55454
  var FOLDERS = ["memory", "context"];
55265
55455
  function checkouts(projectId) {
55266
55456
  const root = join5(homedir5(), ".todoforai");
55267
- const project = projectId && join5(root, "projects", projectId);
55268
- const scopes = [[root, ""], ...project ? [[project, `projects/${projectId}/`], [join5(project, "me"), `projects/${projectId}/me/`]] : []];
55457
+ if (!projectId)
55458
+ throw new Error("Select a project with --project <id> or $TODOFORAI_PROJECT_ID.");
55459
+ const project = join5(root, "projects", projectId);
55460
+ const scopes = [[project, `projects/${projectId}/`], [join5(project, "me"), `projects/${projectId}/me/`]];
55269
55461
  return scopes.flatMap(([dir, remote]) => FOLDERS.map((f) => [join5(dir, f), `${remote}${f}.git`]));
55270
55462
  }
55271
55463
  function git(args, cwd) {
@@ -56226,6 +56418,7 @@ __export(exports_util, {
56226
56418
  toZod: () => toZod,
56227
56419
  stringifyPrimitive: () => stringifyPrimitive,
56228
56420
  slugify: () => slugify,
56421
+ sizeCaps: () => sizeCaps,
56229
56422
  shallowClone: () => shallowClone,
56230
56423
  safeExtend: () => safeExtend,
56231
56424
  required: () => required,
@@ -56800,6 +56993,18 @@ function prefixIssues(path4, issues) {
56800
56993
  function unwrapMessage(message) {
56801
56994
  return typeof message === "string" ? message : message?.message;
56802
56995
  }
56996
+ function sizeCaps(defChecks) {
56997
+ const caps = [];
56998
+ for (const ch of defChecks ?? []) {
56999
+ const d = ch._zod.def;
57000
+ if (!/_(length|size)$|_equals$/.test(d.check))
57001
+ break;
57002
+ const max = d.maximum ?? d.length ?? d.size;
57003
+ if (max >= 0)
57004
+ caps.push([max, ch]);
57005
+ }
57006
+ return caps;
57007
+ }
56803
57008
  function attachSchema(issues, start, inst) {
56804
57009
  var _a;
56805
57010
  for (let i = start;i < issues.length; i++) {
@@ -58219,7 +58424,7 @@ ${content.join(`
58219
58424
  var version = {
58220
58425
  major: 4,
58221
58426
  minor: 6,
58222
- patch: 5
58427
+ patch: 7
58223
58428
  };
58224
58429
 
58225
58430
  // node_modules/zod/v4/core/schemas.js
@@ -58968,6 +59173,31 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
58968
59173
  return payload;
58969
59174
  };
58970
59175
  });
59176
+ function capSize(inst, size) {
59177
+ const caps = sizeCaps(inst._zod.def.checks);
59178
+ if (!caps.length)
59179
+ return;
59180
+ const cap = Math.min(...caps.map((c3) => c3[0]));
59181
+ const parse2 = inst._zod.parse;
59182
+ inst._zod.parse = (payload, ctx) => {
59183
+ const n = size(payload.value);
59184
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
59185
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
59186
+ if (n > max && ch._zod.def.when(payload)) {
59187
+ const start = payload.issues.length;
59188
+ ch._zod.check(payload);
59189
+ payload.issues[start].continue = false;
59190
+ attachSchema(payload.issues, start, inst);
59191
+ return payload;
59192
+ }
59193
+ }
59194
+ }
59195
+ return parse2(payload, ctx);
59196
+ };
59197
+ }
59198
+ var arraySize = (input) => Array.isArray(input) ? input.length : -1;
59199
+ var mapSize = (input) => input instanceof Map ? input.size : -1;
59200
+ var setSize = (input) => input instanceof Set ? input.size : -1;
58971
59201
  function handleArrayResult(result, final, index) {
58972
59202
  if (result.issues.length) {
58973
59203
  final.issues.push(...prefixIssues(index, result.issues));
@@ -59011,6 +59241,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
59011
59241
  }
59012
59242
  return payload;
59013
59243
  };
59244
+ capSize(inst, arraySize);
59014
59245
  });
59015
59246
  function handlePropertyResult(result, final, key, input, optin, optout) {
59016
59247
  const isPresent = key in input;
@@ -59965,6 +60196,7 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
59965
60196
  return Promise.all(proms).then(() => payload);
59966
60197
  return payload;
59967
60198
  };
60199
+ capSize(inst, mapSize);
59968
60200
  });
59969
60201
  function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
59970
60202
  if (keyResult.issues.length) {
@@ -60031,6 +60263,7 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
60031
60263
  return Promise.all(proms).then(() => payload);
60032
60264
  return payload;
60033
60265
  };
60266
+ capSize(inst, setSize);
60034
60267
  });
60035
60268
  function handleSetResult(result, final) {
60036
60269
  if (result.issues.length) {
@@ -69480,9 +69713,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
69480
69713
  doc.write(`}`);
69481
69714
  return outputVar;
69482
69715
  }
69716
+ function generateSizeGate(doc, schema, size) {
69717
+ for (const [max, check] of sizeCaps(schema._zod.def.checks)) {
69718
+ doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
69719
+ }
69720
+ }
69483
69721
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
69484
69722
  const def = schema._zod.def;
69485
69723
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
69724
+ generateSizeGate(doc, schema, `${accessor}.length`);
69486
69725
  const outputVar = buildsValue ? newVar(ctx) : null;
69487
69726
  const iVar = newVar(ctx);
69488
69727
  const elemVar = newVar(ctx);
@@ -69882,6 +70121,7 @@ function literalPropertyKey(ctx, key) {
69882
70121
  function generateMapCheck(doc, ctx, schema, accessor) {
69883
70122
  const def = schema._zod.def;
69884
70123
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
70124
+ generateSizeGate(doc, schema, `${accessor}.size`);
69885
70125
  const outputVar = newVar(ctx);
69886
70126
  const kVar = newVar(ctx);
69887
70127
  const valVar = newVar(ctx);
@@ -69898,6 +70138,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
69898
70138
  function generateSetCheck(doc, ctx, schema, accessor) {
69899
70139
  const def = schema._zod.def;
69900
70140
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
70141
+ generateSizeGate(doc, schema, `${accessor}.size`);
69901
70142
  const outputVar = newVar(ctx);
69902
70143
  const valVar = newVar(ctx);
69903
70144
  doc.write(`const ${outputVar} = new Set();`);
@@ -75065,6 +75306,21 @@ function preserveCustomPayload(schema, key, knownTags) {
75065
75306
  return output2;
75066
75307
  });
75067
75308
  }
75309
+ function defaultOnErrorOptionalStringTag(schema, key) {
75310
+ return exports_external.preprocess((value) => {
75311
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
75312
+ return value;
75313
+ }
75314
+ const record2 = value;
75315
+ const tag = record2[key];
75316
+ if (tag === undefined || tag === null || typeof tag === "string") {
75317
+ return value;
75318
+ }
75319
+ const normalized = { ...record2 };
75320
+ delete normalized[key];
75321
+ return normalized;
75322
+ }, schema);
75323
+ }
75068
75324
  function vecSkipError(itemSchema) {
75069
75325
  return exports_external.array(itemSchema.catch(skippedItem)).transform((items) => items.filter((item) => item !== skippedItem));
75070
75326
  }
@@ -75083,16 +75339,12 @@ var zWriteTextFileRequest = object({
75083
75339
  var zReadTextFileRequest = object({
75084
75340
  sessionId: zSessionId,
75085
75341
  path: string2(),
75086
- line: defaultOnError(int().gte(0).max(4294967295, {
75342
+ line: int().gte(0).max(4294967295, {
75087
75343
  error: "Invalid value: Expected uint32 to be <= 4294967295"
75088
- }).nullish(), () => {
75089
- return;
75090
- }),
75091
- limit: defaultOnError(int().gte(0).max(4294967295, {
75344
+ }).nullish(),
75345
+ limit: int().gte(0).max(4294967295, {
75092
75346
  error: "Invalid value: Expected uint32 to be <= 4294967295"
75093
- }).nullish(), () => {
75094
- return;
75095
- }),
75347
+ }).nullish(),
75096
75348
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75097
75349
  return;
75098
75350
  })
@@ -75343,14 +75595,10 @@ var zEnvVariable = object({
75343
75595
  var zCreateTerminalRequest = object({
75344
75596
  sessionId: zSessionId,
75345
75597
  command: string2(),
75346
- args: defaultOnError(vecSkipError(string2()).optional(), () => []),
75347
- env: defaultOnError(vecSkipError(zEnvVariable).optional(), () => []),
75348
- cwd: defaultOnError(string2().nullish(), () => {
75349
- return;
75350
- }),
75351
- outputByteLimit: defaultOnError(number2().nullish(), () => {
75352
- return;
75353
- }),
75598
+ args: array(string2()).optional(),
75599
+ env: array(zEnvVariable).optional(),
75600
+ cwd: string2().nullish(),
75601
+ outputByteLimit: number2().nullish(),
75354
75602
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75355
75603
  return;
75356
75604
  })
@@ -75876,10 +76124,8 @@ var zAuthMethodTerminal = object({
75876
76124
  description: defaultOnError(string2().nullish(), () => {
75877
76125
  return;
75878
76126
  }),
75879
- args: defaultOnError(vecSkipError(string2()).optional(), () => []),
75880
- env: defaultOnError(record(string2(), string2()).optional(), () => {
75881
- return;
75882
- }),
76127
+ args: array(string2()).optional(),
76128
+ env: record(string2(), string2()).optional(),
75883
76129
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75884
76130
  return;
75885
76131
  })
@@ -76577,17 +76823,61 @@ var zRunningStateUpdate = object({
76577
76823
  return;
76578
76824
  })
76579
76825
  });
76580
- var zIdleStateUpdate = object({
76581
- stopReason: defaultOnError(zStopReason.nullish(), () => {
76826
+ var zErrorStopReason = object({
76827
+ error: defaultOnError(zError.nullish(), () => {
76582
76828
  return;
76829
+ })
76830
+ });
76831
+ var zIdleStateUpdate = defaultOnErrorOptionalStringTag(preserveCustomPayload(intersection(union([
76832
+ object({
76833
+ stopReason: literal("end_turn")
76834
+ }),
76835
+ object({
76836
+ stopReason: literal("max_tokens")
76583
76837
  }),
76838
+ object({
76839
+ stopReason: literal("max_turn_requests")
76840
+ }),
76841
+ object({
76842
+ stopReason: literal("refusal")
76843
+ }),
76844
+ object({
76845
+ stopReason: literal("cancelled")
76846
+ }),
76847
+ zErrorStopReason.and(object({
76848
+ stopReason: literal("error")
76849
+ })),
76850
+ excludeKnownTags(object({
76851
+ stopReason: string2()
76852
+ }), "stopReason", [
76853
+ "cancelled",
76854
+ "end_turn",
76855
+ "error",
76856
+ "max_tokens",
76857
+ "max_turn_requests",
76858
+ "refusal"
76859
+ ]),
76860
+ object({
76861
+ stopReason: _null3().optional()
76862
+ })
76863
+ ]), object({
76584
76864
  usage: defaultOnError(zUsage.nullish(), () => {
76585
76865
  return;
76586
76866
  }),
76587
76867
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76588
76868
  return;
76869
+ }),
76870
+ stopReason: defaultOnError(string2().nullish(), () => {
76871
+ return;
76589
76872
  })
76590
- });
76873
+ })), "stopReason", [
76874
+ "cancelled",
76875
+ "end_turn",
76876
+ "error",
76877
+ "max_tokens",
76878
+ "max_turn_requests",
76879
+ "refusal"
76880
+ ]), "stopReason");
76591
76881
  var zRequiresActionStateUpdate = object({
76592
76882
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76593
76883
  return;
@@ -76970,16 +77260,16 @@ var zMcpServer = union([
76970
77260
  ]);
76971
77261
  var zNewSessionRequest = object({
76972
77262
  cwd: string2(),
76973
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
76974
- mcpServers: requiredDefaultOnError(vecSkipError(zMcpServer), () => []),
77263
+ additionalDirectories: array(string2()).optional(),
77264
+ mcpServers: array(zMcpServer),
76975
77265
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76976
77266
  return;
76977
77267
  })
76978
77268
  });
76979
77269
  var zLoadSessionRequest = object({
76980
- mcpServers: requiredDefaultOnError(vecSkipError(zMcpServer), () => []),
77270
+ mcpServers: array(zMcpServer),
76981
77271
  cwd: string2(),
76982
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77272
+ additionalDirectories: array(string2()).optional(),
76983
77273
  sessionId: zSessionId,
76984
77274
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76985
77275
  return;
@@ -77001,8 +77291,8 @@ var zDeleteSessionRequest = object({
77001
77291
  var zForkSessionRequest = object({
77002
77292
  sessionId: zSessionId,
77003
77293
  cwd: string2(),
77004
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77005
- mcpServers: defaultOnError(vecSkipError(zMcpServer).optional(), () => []),
77294
+ additionalDirectories: array(string2()).optional(),
77295
+ mcpServers: array(zMcpServer).optional(),
77006
77296
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77007
77297
  return;
77008
77298
  })
@@ -77010,8 +77300,8 @@ var zForkSessionRequest = object({
77010
77300
  var zResumeSessionRequest = object({
77011
77301
  sessionId: zSessionId,
77012
77302
  cwd: string2(),
77013
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77014
- mcpServers: defaultOnError(vecSkipError(zMcpServer).optional(), () => []),
77303
+ additionalDirectories: array(string2()).optional(),
77304
+ mcpServers: array(zMcpServer).optional(),
77015
77305
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77016
77306
  return;
77017
77307
  })
@@ -77392,7 +77682,7 @@ var zDidChangeDocumentNotification = object({
77392
77682
  sessionId: zSessionId,
77393
77683
  uri: string2(),
77394
77684
  version: number2(),
77395
- contentChanges: requiredDefaultOnError(vecSkipError(zTextDocumentContentChangeEvent), () => []),
77685
+ contentChanges: array(zTextDocumentContentChangeEvent),
77396
77686
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77397
77687
  return;
77398
77688
  })
@@ -78471,9 +78761,26 @@ var zGuardElicitationPropertySchemaBoolean = zBooleanPropertySchema.and(object({
78471
78761
  var zGuardElicitationPropertySchemaArray = zMultiSelectPropertySchema.and(object({ type: literal("array") }));
78472
78762
  var zGuardMultiSelectItemsString = zStringMultiSelectItems.and(object({ type: literal("string") }));
78473
78763
  var zGuardStateUpdateRunning = zRunningStateUpdate.and(object({ state: literal("running") }));
78474
- var zGuardStateUpdateIdle = zIdleStateUpdate.and(object({ state: literal("idle") }));
78764
+ var zGuardStateUpdateIdle = zIdleStateUpdate.and(object({ usage: zUsage.nullish() })).and(object({ state: literal("idle") }));
78475
78765
  var zGuardStateUpdateRequiresAction = zRequiresActionStateUpdate.and(object({ state: literal("requires_action") }));
78476
78766
  var zGuardStateUpdateUnknown = zUnknownStateUpdate.and(object({ state: literal("unknown") }));
78767
+ var zGuardIdleStateUpdateEndTurn = object({
78768
+ stopReason: literal("end_turn")
78769
+ });
78770
+ var zGuardIdleStateUpdateMaxTokens = object({
78771
+ stopReason: literal("max_tokens")
78772
+ });
78773
+ var zGuardIdleStateUpdateMaxTurnRequests = object({
78774
+ stopReason: literal("max_turn_requests")
78775
+ });
78776
+ var zGuardIdleStateUpdateRefusal = object({
78777
+ stopReason: literal("refusal")
78778
+ });
78779
+ var zGuardIdleStateUpdateCancelled = object({
78780
+ stopReason: literal("cancelled")
78781
+ });
78782
+ var zGuardIdleStateUpdateError = zErrorStopReason.and(object({ error: zError.nullish() })).and(object({ stopReason: literal("error") }));
78783
+ var zGuardIdleStateUpdateNone = object({ stopReason: _null3().optional() });
78477
78784
  var zGuardCreateElicitationResponseAccept = zElicitationAcceptAction.and(object({ action: literal("accept") }));
78478
78785
  var zGuardCreateElicitationResponseDecline = object({
78479
78786
  action: literal("decline")
@@ -79832,6 +80139,7 @@ async function runAcp(apiUrl, apiKey, opts) {
79832
80139
 
79833
80140
  // src/shutdown.ts
79834
80141
  var activeRun2 = null;
80142
+ var stopOnExit2 = false;
79835
80143
  var cleanups2 = [];
79836
80144
  var exiting2 = null;
79837
80145
  var cancelling2 = null;
@@ -79839,6 +80147,11 @@ var CANCEL_TIMEOUT_MS2 = 5000;
79839
80147
  function setActiveRun2(run2) {
79840
80148
  activeRun2 = run2;
79841
80149
  }
80150
+ function stopRunOnExit() {
80151
+ stopOnExit2 = true;
80152
+ }
80153
+ var detachNotice2 = (todoId) => `Todo keeps running — reattach: tfa-cli --resume ${todoId}
80154
+ `;
79842
80155
  function trackStart(ws, projectId, started) {
79843
80156
  setActiveRun2(started.then((todoId) => ({ ws, projectId, todoId }), () => null));
79844
80157
  }
@@ -79856,7 +80169,7 @@ function cancelActiveRun2(timeoutMs = CANCEL_TIMEOUT_MS2) {
79856
80169
  if (cancelling2)
79857
80170
  return cancelling2;
79858
80171
  const pending = activeRun2;
79859
- if (!pending)
80172
+ if (!pending || !stopOnExit2)
79860
80173
  return Promise.resolve({ ok: false, todoId: null });
79861
80174
  const res = { ok: false };
79862
80175
  const attempt = (async () => {
@@ -79882,7 +80195,11 @@ function shutdown2(code, reason) {
79882
80195
  process.stderr.write(`
79883
80196
  ${reason}
79884
80197
  `);
79885
- if (activeRun2) {
80198
+ if (activeRun2 && !stopOnExit2) {
80199
+ const run2 = await withTimeout2(Promise.resolve(activeRun2), CANCEL_TIMEOUT_MS2, null);
80200
+ if (run2)
80201
+ process.stderr.write(detachNotice2(run2.todoId));
80202
+ } else if (activeRun2) {
79886
80203
  const { ok, todoId } = await cancelActiveRun2();
79887
80204
  if (ok)
79888
80205
  process.stderr.write(`Stopped todo ${todoId}
@@ -79912,7 +80229,7 @@ function finish(code) {
79912
80229
  });
79913
80230
  }
79914
80231
  function installSignalHandlers() {
79915
- process.on("SIGINT", () => void shutdown2(130, "Cancelled by user (Ctrl+C)"));
80232
+ process.on("SIGINT", () => void shutdown2(130, "Exited (Ctrl+C)"));
79916
80233
  process.on("SIGTERM", () => void shutdown2(143, "Terminated (SIGTERM)"));
79917
80234
  process.on("SIGHUP", () => void shutdown2(129, "Hangup (SIGHUP)"));
79918
80235
  }
@@ -80632,8 +80949,7 @@ async function main() {
80632
80949
  await watchTodo(ws2, todoId, projectId2, {
80633
80950
  json: !!args.json,
80634
80951
  autoApprove,
80635
- agentSettings: agent3,
80636
- exitOnInterrupt: !!args["non-interactive"]
80952
+ agentSettings: agent3
80637
80953
  });
80638
80954
  if (!args["non-interactive"]) {
80639
80955
  process.stderr.write(`
@@ -80684,7 +81000,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80684
81000
  cfg.addToHistory(followUp);
80685
81001
  await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
80686
81002
  await linkToSpawningBlock(api2, todoId);
80687
- await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3, exitOnInterrupt: !!args["non-interactive"] });
81003
+ await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3 });
80688
81004
  }
80689
81005
  if (!args["non-interactive"]) {
80690
81006
  await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
@@ -80706,6 +81022,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80706
81022
  if (mayfly) {
80707
81023
  process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
80708
81024
  `);
81025
+ stopRunOnExit();
80709
81026
  onShutdown(mayfly.stop);
80710
81027
  mayfly.child.on("exit", (code, sig) => {
80711
81028
  if (!mayfly.stopped())
@@ -80845,8 +81162,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80845
81162
  await watchTodo(ws, actualTodoId, projectId, {
80846
81163
  json: !!args.json,
80847
81164
  autoApprove,
80848
- agentSettings: agent2,
80849
- exitOnInterrupt: !!args["non-interactive"]
81165
+ agentSettings: agent2
80850
81166
  });
80851
81167
  if (!args["non-interactive"]) {
80852
81168
  process.stderr.write(`
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/todoforai/cli.git"
8
8
  },
9
- "version": "0.1.66",
9
+ "version": "0.1.68",
10
10
  "type": "module",
11
11
  "bin": {
12
12
  "todoforai-cli": "bin/todoforai-cli.js",