@todoforai/cli 0.1.66 → 0.1.67

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: "",
@@ -45173,7 +45199,7 @@ var tool_catalog_default = {
45173
45199
  installer: "npm",
45174
45200
  label: "TODOforAI",
45175
45201
  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.',
45202
+ 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
45203
  installCmd: "npm install -g @todoforai/cli",
45178
45204
  versionCmd: "tfa-cli --version 2>/dev/null | sed -n 1p",
45179
45205
  preinstallCloud: true,
@@ -49944,21 +49970,67 @@ var tool_catalog_default = {
49944
49970
  },
49945
49971
  statusCmd: `zoo user view --format json 2>/dev/null | sed -n 's/.*"email": *"\\([^"]*\\)".*/\\1/p' | grep -m1 .`,
49946
49972
  loginCmd: "zoo auth login --web",
49973
+ logoutCmd: "printf 'y\\n' | zoo --host api.zoo.dev auth logout",
49947
49974
  loginNeedsPty: true,
49948
49975
  deviceCodePattern: "user code: ([A-Z0-9]{4}-[A-Z0-9]{4})",
49949
- oauthUrlPattern: "(https://api\\.zoo\\.dev/oauth2/device/verify\\S*)",
49976
+ oauthUrlPattern: "(https://api\\.zoo\\.dev/oauth2/device/verify\\?[^\\s']+)",
49950
49977
  canStreamLogin: true,
49951
49978
  credentialPaths: [
49952
49979
  "~/.config/zoo/hosts.toml"
49953
49980
  ],
49954
49981
  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
49982
  versionCmd: "zoo --version 2>/dev/null | sed -n 1p"
49983
+ },
49984
+ "python-docx": {
49985
+ category: "documents",
49986
+ pkg: "python-docx",
49987
+ pipModules: ["docx"],
49988
+ installer: "pip",
49989
+ label: "Word documents",
49990
+ summary: "Create, read and edit Word documents.",
49991
+ 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`,
49992
+ description: "Python python-docx library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
49993
+ },
49994
+ openpyxl: {
49995
+ category: "documents",
49996
+ pkg: "openpyxl",
49997
+ pipModules: ["openpyxl"],
49998
+ installer: "pip",
49999
+ label: "Excel workbooks",
50000
+ summary: "Create, read and edit Excel workbooks.",
50001
+ 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`,
50002
+ description: "Python openpyxl library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
50003
+ },
50004
+ "python-pptx": {
50005
+ category: "documents",
50006
+ pkg: "python-pptx",
50007
+ pipModules: ["pptx"],
50008
+ installer: "pip",
50009
+ label: "PowerPoint decks",
50010
+ summary: "Create, read and edit PowerPoint decks.",
50011
+ 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`,
50012
+ description: "Python python-pptx library. Use python3 to create, save, reopen and edit Office files; no Microsoft account required."
50013
+ },
50014
+ "modal-h3": {
50015
+ category: "creative",
50016
+ pkg: "@todoforai/modal-h3",
50017
+ installer: "npm",
50018
+ label: "MiniMax-H3 on Modal",
50019
+ summary: "Generate videos with sound using MiniMax-H3 on your own Modal GPUs.",
50020
+ versionCmd: "modal-h3 --version 2>/dev/null | sed -n 1p",
50021
+ statusCmd: `modal-h3 whoami 2>/dev/null | grep '"authenticated": true' >/dev/null && echo authenticated`,
50022
+ loginCmd: "printf '%s' <proxy_token> | modal-h3 auth --token-stdin --fl2va-url <fl2va_url> --ref2va-url <ref2va_url>",
50023
+ credentialPaths: ["~/.config/modal-h3/credentials.json"],
50024
+ 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
50025
  }
49957
50026
  };
49958
50027
 
49959
50028
  // ../packages/shared-fbe/src/toolCatalog.ts
49960
50029
  var TOOL_CATALOG = tool_catalog_default;
49961
50030
  // ../packages/shared-fbe/src/toolInstallCommand.ts
50031
+ var NPM_HOME = '"${TFA_NPM_HOME:-$HOME}"';
50032
+ var NPM_TOOLS = NPM_HOME.replace('}"', '}/.todoforai/tools"');
50033
+ var NPM_LOCAL = NPM_HOME.replace('}"', '}/.local"');
49962
50034
  var KEY_OF = new Map(Object.entries(TOOL_CATALOG).map(([k, e]) => [e, k]));
49963
50035
  var NODE_MIN_MAJOR = 24;
49964
50036
  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 +50272,7 @@ var package_default = {
50200
50272
  type: "git",
50201
50273
  url: "git+https://github.com/todoforai/cli.git"
50202
50274
  },
50203
- version: "0.1.66",
50275
+ version: "0.1.67",
50204
50276
  type: "module",
50205
50277
  bin: {
50206
50278
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -50330,6 +50402,13 @@ Usage:
50330
50402
 
50331
50403
  todo-id defaults to $TODOFORAI_TODO_ID (agent shell) or the last todo this CLI touched.
50332
50404
 
50405
+ Viewer chrome:
50406
+ --title appears in the header; a kebab menu holds file actions, versions and sharing when available.
50407
+ Desktop floating previews add minimize (park in a tab), maximize/restore and close (X).
50408
+ Inline blocks have no window controls. Do not draw these controls or a duplicate title bar in authored HTML.
50409
+ The floating viewer owns the rounded frame. Keep the HTML root flat, without a border, radius, shadow or outer margin;
50410
+ internal content padding and spacing are fine. Existing files and websites keep their own styling.
50411
+
50333
50412
  When to use what:
50334
50413
  read once, move on → plain chat reply
50335
50414
  interactive, this chat only → html_snippet (can exec/connect/chat back)
@@ -51404,15 +51483,20 @@ function blockToAnthropic(block, mode, format) {
51404
51483
  const msg = block.error_message || block.content || "(unknown error)";
51405
51484
  return [{ type: "text", text: `[error] ${msg}` }];
51406
51485
  }
51486
+ const name = t === "named_tool" ? block.tool_name === "run_shell" ? "bash" : block.tool_name : t;
51407
51487
  const inputFields = [];
51408
51488
  for (const [k, v] of Object.entries(block)) {
51409
- if (BLOCK_META_KEYS.has(k))
51489
+ if (BLOCK_META_KEYS.has(k) || k === "args" || t === "named_tool" && k === "tool_name")
51410
51490
  continue;
51411
51491
  if (v === undefined || v === null || v === "")
51412
51492
  continue;
51413
51493
  inputFields.push([k, v]);
51414
51494
  }
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("")}/>` };
51495
+ for (const [k, v] of Object.entries(block.args ?? {})) {
51496
+ if (!inputFields.some(([key]) => key === k))
51497
+ inputFields.push([k, v]);
51498
+ }
51499
+ 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
51500
  if (format === "compact" && mode !== "default")
51417
51501
  use.id = block.id;
51418
51502
  const items = [use];
@@ -51428,8 +51512,9 @@ function blockToAnthropic(block, mode, format) {
51428
51512
  source.name = att.originalName;
51429
51513
  parts.push({ type: isImage ? "image" : "document", source });
51430
51514
  }
51431
- if (typeof r.content === "string" && r.content)
51432
- parts.push({ type: "text", text: r.content });
51515
+ const text = r.content || r.meta?.text;
51516
+ if (typeof text === "string" && text)
51517
+ parts.push({ type: "text", text });
51433
51518
  return parts;
51434
51519
  });
51435
51520
  const result = { type: "tool_result", tool_use_id: block.id };
@@ -51446,8 +51531,9 @@ function blockToAnthropic(block, mode, format) {
51446
51531
  const nameAttr = att.originalName && !isAutoName ? ` name=${xmlAttr(att.originalName)}` : "";
51447
51532
  parts.push(`<attachment uri=${xmlAttr(att.uri)}${nameAttr} size=${att.fileSize}/>`);
51448
51533
  }
51449
- if (typeof r.content === "string" && r.content)
51450
- parts.push(r.content);
51534
+ const text = r.content || r.meta?.text;
51535
+ if (typeof text === "string" && text)
51536
+ parts.push(text);
51451
51537
  }
51452
51538
  const result = { type: "tool_result", content: parts.join(`
51453
51539
  `) };
@@ -52673,6 +52759,7 @@ function renderDiff(originalContent, modifiedContent, filePath) {
52673
52759
 
52674
52760
  // src/shutdown.ts
52675
52761
  var activeRun = null;
52762
+ var stopOnExit = false;
52676
52763
  var cleanups = [];
52677
52764
  var exiting = null;
52678
52765
  var cancelling = null;
@@ -52680,6 +52767,9 @@ var CANCEL_TIMEOUT_MS = 5000;
52680
52767
  function setActiveRun(run) {
52681
52768
  activeRun = run;
52682
52769
  }
52770
+ var stopsRunOnExit = () => stopOnExit;
52771
+ var detachNotice = (todoId) => `Todo keeps running — reattach: tfa-cli --resume ${todoId}
52772
+ `;
52683
52773
  function withTimeout(p, ms, fallback) {
52684
52774
  let timer;
52685
52775
  const timeout = new Promise((r) => {
@@ -52691,7 +52781,7 @@ function cancelActiveRun(timeoutMs = CANCEL_TIMEOUT_MS) {
52691
52781
  if (cancelling)
52692
52782
  return cancelling;
52693
52783
  const pending = activeRun;
52694
- if (!pending)
52784
+ if (!pending || !stopOnExit)
52695
52785
  return Promise.resolve({ ok: false, todoId: null });
52696
52786
  const res = { ok: false };
52697
52787
  const attempt = (async () => {
@@ -52717,7 +52807,11 @@ function shutdown(code, reason) {
52717
52807
  process.stderr.write(`
52718
52808
  ${reason}
52719
52809
  `);
52720
- if (activeRun) {
52810
+ if (activeRun && !stopOnExit) {
52811
+ const run = await withTimeout(Promise.resolve(activeRun), CANCEL_TIMEOUT_MS, null);
52812
+ if (run)
52813
+ process.stderr.write(detachNotice(run.todoId));
52814
+ } else if (activeRun) {
52721
52815
  const { ok, todoId } = await cancelActiveRun();
52722
52816
  if (ok)
52723
52817
  process.stderr.write(`Stopped todo ${todoId}
@@ -52829,20 +52923,6 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
52829
52923
  diffStoreByWs.set(ws, diffStore);
52830
52924
  const diffRendered = new Set;
52831
52925
  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
52926
  const pendingBlocks = [];
52847
52927
  let approvalPromptActive = false;
52848
52928
  let activeApprovalBlocks = [];
@@ -53065,14 +53145,10 @@ ${YELLOW}Connection lost — reconnecting...${RESET}
53065
53145
  await cancelActiveRun();
53066
53146
  process.exitCode = 1;
53067
53147
  if (!opts.suppressCancelNotice) {
53068
- process.stderr.write(`${YELLOW}Interrupted${RESET}
53069
- `);
53148
+ process.stderr.write(stopsRunOnExit() ? `${YELLOW}Interrupted${RESET}
53149
+ ` : detachNotice(todoId));
53070
53150
  }
53071
53151
  return false;
53072
- } finally {
53073
- process.removeAllListeners("SIGINT");
53074
- for (const fn of origHandler)
53075
- process.on("SIGINT", fn);
53076
53152
  }
53077
53153
  }
53078
53154
 
@@ -53641,6 +53717,8 @@ Usage:
53641
53717
  tfa-cli project settings <field=value>… Project settings (PUT /settings/projects/{id})
53642
53718
  tfa-cli project groups List todo groups
53643
53719
  tfa-cli project groups set <slug> <field=value>… Create/update a group: name description pinned archived order
53720
+ tfa-cli project groups intake <slug> [on|off|regenerate] [field=value]…
53721
+ Public intake: autoStart maxSubmissions postIpLimit placeholder agentSettingsId
53644
53722
  tfa-cli project groups reorder <slug>… Set wall order
53645
53723
  tfa-cli project surfaces List the board's card surfaces (live refs shown as cards)
53646
53724
  tfa-cli project surfaces set <id> ref=<ref> [title=…] [group=<slug>] [order=N]
@@ -53805,6 +53883,25 @@ async function projectCommand(api, positionals, args, projectId) {
53805
53883
  `);
53806
53884
  return;
53807
53885
  }
53886
+ if (verb === "intake") {
53887
+ const [slug, ...options] = gargs;
53888
+ if (!slug)
53889
+ fail("Usage: tfa-cli project groups intake <slug> [on|off|regenerate] [field=value]…");
53890
+ const action = ["on", "off", "regenerate"].includes(options[0]) ? options.shift() : undefined;
53891
+ if (options.length && action === "regenerate")
53892
+ fail("regenerate does not accept settings");
53893
+ const fields = parseAssignments2(options);
53894
+ 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);
53895
+ if (args.json) {
53896
+ console.log(JSON.stringify(intake, null, 2));
53897
+ return;
53898
+ }
53899
+ process.stderr.write(`${slug}: public ${intake.enabled ? "on" : "off"}, ${intake.submissionCount}/${intake.maxSubmissions}, autoStart=${intake.autoStart}
53900
+ `);
53901
+ if (intake.url)
53902
+ console.log(intake.url);
53903
+ return;
53904
+ }
53808
53905
  if (verb === "reorder") {
53809
53906
  if (!gargs.length)
53810
53907
  fail("Usage: tfa-cli project groups reorder <slug>…");
@@ -53880,11 +53977,10 @@ Usage:
53880
53977
  tfa-cli brand voice correct "<what is off>" Tell the learner what it got wrong; profile is updated
53881
53978
  in one pass and the correction is kept for every re-learn
53882
53979
 
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.
53980
+ Every voice verb takes --scope mine|project — which voice to teach:
53981
+ mine yours, only in this project (default)
53982
+ project the project's, shared with its members (needs write access)
53983
+ On the same voice name the narrower wins: mine > project.
53888
53984
 
53889
53985
 
53890
53986
  Questions the UI asks (keys for 'answers'):
@@ -53984,9 +54080,9 @@ async function voiceDeviceCommand(rest, args) {
53984
54080
  }
53985
54081
  async function voiceCommand(api, projectId, rest, args) {
53986
54082
  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}'`);
54083
+ const scope = args.scope ?? "mine";
54084
+ if (!["project", "mine"].includes(scope))
54085
+ fail(`--scope must be mine or project, got '${scope}'`);
53990
54086
  if (verb === "collect" && !vargs[0])
53991
54087
  fail("Usage: tfa-cli brand voice collect <channel> [--url U | --account <page-id> | --max N] [--dry-run]");
53992
54088
  if (verb === "answers") {
@@ -54113,6 +54209,7 @@ Usage:
54113
54209
  tfa-cli device access <device> who you shared the device with
54114
54210
  tfa-cli device share <device> <email> let a teammate use it (their runs act as THEM)
54115
54211
  tfa-cli device unshare <device> <email>
54212
+ tfa-cli device leave <device> remove your own access to a shared device
54116
54213
 
54117
54214
  <device> is an id, name or hostname (unique partial works).
54118
54215
  Devices shared with you are marked "shared"; only the owner can rename/share them.
@@ -54149,6 +54246,12 @@ async function deviceCommand(api, positionals, args) {
54149
54246
  fail("Usage: tfa-cli device rename <device> <name>");
54150
54247
  await api.renameDevice(found.id, value);
54151
54248
  process.stderr.write(`${GREEN}✅ ${found.name} → ${value}${RESET}
54249
+ `);
54250
+ return;
54251
+ }
54252
+ if (sub === "leave") {
54253
+ await api.leaveDevice(found.id);
54254
+ process.stderr.write(`${GREEN}✅ Removed ${found.name} from your devices${RESET}
54152
54255
  `);
54153
54256
  return;
54154
54257
  }
@@ -54198,15 +54301,7 @@ var STATUS_COLOR = {
54198
54301
  POSTPONED: CYAN,
54199
54302
  PAUSED: CYAN
54200
54303
  };
54201
- var CLOSED = new Set([
54202
- "DONE",
54203
- "READY_CHECKED",
54204
- "CANCELLED",
54205
- "CANCELLED_CHECKED",
54206
- "ERROR_CHECKED",
54207
- "ARCHIVED",
54208
- "DELETED"
54209
- ]);
54304
+ var CLOSED = new Set(CLOSED_STATUSES);
54210
54305
  var VALID_STATUS = new Set(Object.values(TodoStatus));
54211
54306
  var isOpen = (s) => !CLOSED.has(s);
54212
54307
  function printListTodosHelp() {
@@ -54218,11 +54313,16 @@ Usage:
54218
54313
 
54219
54314
  Flags:
54220
54315
  -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)
54316
+ --cursor <n> Fetch todos older than this cursor (sort field, epoch ms)
54317
+ --since <t> Only todos with sort field >= t (inclusive)
54318
+ --until <t> Only todos with sort field < t (exclusive)
54319
+ t = epoch ms | YYYY-MM-DD (local midnight) | ISO datetime | 12h / 3d ago
54320
+ --sort <key> activity (lastActivityAt, default) | created (createdAt)
54321
+ --contributor <u> Only todos a member opened/wrote in (id or email; comma = any)
54322
+ --page-size <n> Backend page size per request (default: min(limit, 400), max: 400)
54223
54323
  --search <text> Search content/status on the backend
54224
54324
  -s, --status <S[,S2]> Filter by status (comma-separated, union).
54225
- -A, --all Include DONE (also CANCELLED, ARCHIVED, …)
54325
+ -A, --all Include closed (DONE, ARCHIVED, DELETED)
54226
54326
  --project <id> Project ID (default: current default project)
54227
54327
  --json Output { items, nextCursor } as JSON
54228
54328
  -h, --help Show this help
@@ -54233,6 +54333,8 @@ Examples:
54233
54333
  todoforai-cli list --cursor 1719234567890 # next page
54234
54334
  todoforai-cli list --all # include DONE
54235
54335
  todoforai-cli list -s RUNNING,REVIEW_REQUESTED
54336
+ todoforai-cli list -A --since 2026-10-11 --until 2026-10-12 # touched that day
54337
+ todoforai-cli list -A --sort created --since 7d --contributor a@b.com
54236
54338
  todoforai-cli list --search bug --json | jq '.items[].id'
54237
54339
  `);
54238
54340
  }
@@ -54258,6 +54360,25 @@ function cursorValue(value) {
54258
54360
  }
54259
54361
  return n;
54260
54362
  }
54363
+ function parseTime(value, name, now = Date.now()) {
54364
+ if (value === undefined)
54365
+ return;
54366
+ const s = String(value).trim();
54367
+ const rel = s.match(/^(\d+)([mhd])$/);
54368
+ let n;
54369
+ if (/^\d+$/.test(s))
54370
+ n = Number(s);
54371
+ else if (rel)
54372
+ n = now - Number(rel[1]) * { m: 60000, h: 3600000, d: 86400000 }[rel[2]];
54373
+ else
54374
+ n = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00` : s);
54375
+ if (!Number.isFinite(n) || n < 0) {
54376
+ process.stderr.write(`${RED}Error: ${name} must be epoch ms, YYYY-MM-DD, ISO datetime or e.g. 12h / 3d${RESET}
54377
+ `);
54378
+ process.exit(2);
54379
+ }
54380
+ return n;
54381
+ }
54261
54382
  function toItemsPage(response) {
54262
54383
  if (Array.isArray(response))
54263
54384
  return { items: response };
@@ -54266,8 +54387,25 @@ function toItemsPage(response) {
54266
54387
  nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
54267
54388
  };
54268
54389
  }
54269
- function rowTime(t) {
54270
- return Number(t.lastActivityAt ?? t.createdAt ?? 0);
54390
+ function rowTime(t, sort = "activity") {
54391
+ return Number((sort === "created" ? t.createdAt : t.lastActivityAt ?? t.createdAt) ?? 0);
54392
+ }
54393
+ async function resolveContributors(api, projectId, raw) {
54394
+ const wanted = raw.split(",").map((s) => s.trim()).filter(Boolean);
54395
+ if (!wanted.some((w) => w.includes("@")))
54396
+ return wanted;
54397
+ const members = await api.getProjectMembers(projectId);
54398
+ return wanted.map((w) => {
54399
+ if (!w.includes("@"))
54400
+ return w;
54401
+ const m = members.find((m2) => m2.email?.toLowerCase() === w.toLowerCase());
54402
+ if (!m) {
54403
+ process.stderr.write(`${RED}Error: no project member with email ${w}${RESET}
54404
+ `);
54405
+ process.exit(2);
54406
+ }
54407
+ return m.id;
54408
+ });
54271
54409
  }
54272
54410
  function printStatusError(unknown) {
54273
54411
  process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
@@ -54290,6 +54428,10 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54290
54428
  cursor: { type: "string" },
54291
54429
  "page-size": { type: "string" },
54292
54430
  search: { type: "string" },
54431
+ since: { type: "string" },
54432
+ until: { type: "string" },
54433
+ sort: { type: "string" },
54434
+ contributor: { type: "string" },
54293
54435
  status: { type: "string", short: "s" },
54294
54436
  all: { type: "boolean", short: "A", default: false },
54295
54437
  project: { type: "string" },
@@ -54309,9 +54451,20 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54309
54451
  process.exit(1);
54310
54452
  }
54311
54453
  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);
54454
+ const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 400), 400);
54455
+ const sort = values.sort === undefined ? "activity" : String(values.sort);
54456
+ if (sort !== "activity" && sort !== "created") {
54457
+ process.stderr.write(`${RED}Error: --sort must be activity or created${RESET}
54458
+ `);
54459
+ process.exit(2);
54460
+ }
54461
+ const since = parseTime(values.since, "--since");
54462
+ const until = parseTime(values.until, "--until");
54313
54463
  let cursor = cursorValue(values.cursor);
54464
+ if (until !== undefined)
54465
+ cursor = cursor === undefined ? until : Math.min(cursor, until);
54314
54466
  const search = values.search ? String(values.search) : undefined;
54467
+ const contributorIds = values.contributor ? await resolveContributors(api, projectId, String(values.contributor)) : undefined;
54315
54468
  const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
54316
54469
  const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
54317
54470
  if (unknown.length)
@@ -54323,9 +54476,24 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54323
54476
  let backendNextCursor;
54324
54477
  while (rows.length < limit) {
54325
54478
  const requestLimit = Math.min(pageSize, limit - rows.length);
54326
- const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
54479
+ const page = toItemsPage(await api.listTodos(projectId, {
54480
+ limit: requestLimit,
54481
+ cursor,
54482
+ search,
54483
+ since,
54484
+ contributorIds,
54485
+ ...!includeClosed && { excludeClosed: true },
54486
+ ...sort !== "activity" && { sort }
54487
+ }));
54327
54488
  backendNextCursor = page.nextCursor;
54489
+ let exhausted = false;
54328
54490
  for (const t of page.items) {
54491
+ if (since !== undefined && rowTime(t) < since) {
54492
+ exhausted = true;
54493
+ break;
54494
+ }
54495
+ if (since !== undefined && rowTime(t, sort) < since)
54496
+ continue;
54329
54497
  const status = String(t.status).toUpperCase();
54330
54498
  if (!includeClosed && CLOSED.has(status))
54331
54499
  continue;
@@ -54335,14 +54503,18 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54335
54503
  if (rows.length >= limit)
54336
54504
  break;
54337
54505
  }
54506
+ if (exhausted) {
54507
+ backendNextCursor = undefined;
54508
+ break;
54509
+ }
54338
54510
  if (!backendNextCursor || page.items.length === 0)
54339
54511
  break;
54340
54512
  cursor = backendNextCursor;
54341
54513
  }
54342
- rows.sort((a, b) => rowTime(b) - rowTime(a));
54514
+ rows.sort((a, b) => rowTime(b, sort) - rowTime(a, sort));
54343
54515
  const visible = rows.slice(0, limit);
54344
54516
  const hasBufferedRows = rows.length > visible.length;
54345
- const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
54517
+ const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1], sort) : undefined;
54346
54518
  if (values.json) {
54347
54519
  process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
54348
54520
  `);
@@ -54360,7 +54532,7 @@ async function listTodosCommand(api, defaultProjectId, argv) {
54360
54532
  for (const t of visible) {
54361
54533
  const status = String(t.status).toUpperCase();
54362
54534
  const sc = STATUS_COLOR[status] || DIM;
54363
- const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
54535
+ const ts = new Date(rowTime(t, sort)).toISOString().slice(0, 16).replace("T", " ");
54364
54536
  const title = String(t.content ?? "").split(`
54365
54537
  `)[0].slice(0, 100);
54366
54538
  process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
@@ -55248,7 +55420,6 @@ tfa-cli memory — memory and context folders as git checkouts
55248
55420
 
55249
55421
  Usage:
55250
55422
  tfa-cli memory sync [--project <id>] Clone, or pull, into:
55251
- ~/.todoforai/{memory,context} yours (todoforai:memory, todoforai:context)
55252
55423
  ~/.todoforai/projects/<id>/{memory,context} the project's (todoforai:projects/<id>/…)
55253
55424
  ~/.todoforai/projects/<id>/me/{memory,context} yours in it (todoforai:projects/<id>/me/…)
55254
55425
  tfa-cli memory path [--project <id>] Print the checkout paths
@@ -55258,14 +55429,17 @@ use plain git: log, diff, show, revert, commit, then \`git push\` — the push u
55258
55429
  folder. A push behind the server is refused: \`git pull\` first. Files over 5 MB live outside
55259
55430
  history (upload them instead). Project folders: members with write access push; your own in
55260
55431
  a project (me/) is private to you. The project
55261
- defaults to $TODOFORAI_PROJECT_ID.
55432
+ defaults to $TODOFORAI_PROJECT_ID and is required. Legacy global folders are read-only
55433
+ recovery, not synced.
55262
55434
  `);
55263
55435
  }
55264
55436
  var FOLDERS = ["memory", "context"];
55265
55437
  function checkouts(projectId) {
55266
55438
  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/`]] : []];
55439
+ if (!projectId)
55440
+ throw new Error("Select a project with --project <id> or $TODOFORAI_PROJECT_ID.");
55441
+ const project = join5(root, "projects", projectId);
55442
+ const scopes = [[project, `projects/${projectId}/`], [join5(project, "me"), `projects/${projectId}/me/`]];
55269
55443
  return scopes.flatMap(([dir, remote]) => FOLDERS.map((f) => [join5(dir, f), `${remote}${f}.git`]));
55270
55444
  }
55271
55445
  function git(args, cwd) {
@@ -56226,6 +56400,7 @@ __export(exports_util, {
56226
56400
  toZod: () => toZod,
56227
56401
  stringifyPrimitive: () => stringifyPrimitive,
56228
56402
  slugify: () => slugify,
56403
+ sizeCaps: () => sizeCaps,
56229
56404
  shallowClone: () => shallowClone,
56230
56405
  safeExtend: () => safeExtend,
56231
56406
  required: () => required,
@@ -56800,6 +56975,18 @@ function prefixIssues(path4, issues) {
56800
56975
  function unwrapMessage(message) {
56801
56976
  return typeof message === "string" ? message : message?.message;
56802
56977
  }
56978
+ function sizeCaps(defChecks) {
56979
+ const caps = [];
56980
+ for (const ch of defChecks ?? []) {
56981
+ const d = ch._zod.def;
56982
+ if (!/_(length|size)$|_equals$/.test(d.check))
56983
+ break;
56984
+ const max = d.maximum ?? d.length ?? d.size;
56985
+ if (max >= 0)
56986
+ caps.push([max, ch]);
56987
+ }
56988
+ return caps;
56989
+ }
56803
56990
  function attachSchema(issues, start, inst) {
56804
56991
  var _a;
56805
56992
  for (let i = start;i < issues.length; i++) {
@@ -58219,7 +58406,7 @@ ${content.join(`
58219
58406
  var version = {
58220
58407
  major: 4,
58221
58408
  minor: 6,
58222
- patch: 5
58409
+ patch: 7
58223
58410
  };
58224
58411
 
58225
58412
  // node_modules/zod/v4/core/schemas.js
@@ -58968,6 +59155,31 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
58968
59155
  return payload;
58969
59156
  };
58970
59157
  });
59158
+ function capSize(inst, size) {
59159
+ const caps = sizeCaps(inst._zod.def.checks);
59160
+ if (!caps.length)
59161
+ return;
59162
+ const cap = Math.min(...caps.map((c3) => c3[0]));
59163
+ const parse2 = inst._zod.parse;
59164
+ inst._zod.parse = (payload, ctx) => {
59165
+ const n = size(payload.value);
59166
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
59167
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
59168
+ if (n > max && ch._zod.def.when(payload)) {
59169
+ const start = payload.issues.length;
59170
+ ch._zod.check(payload);
59171
+ payload.issues[start].continue = false;
59172
+ attachSchema(payload.issues, start, inst);
59173
+ return payload;
59174
+ }
59175
+ }
59176
+ }
59177
+ return parse2(payload, ctx);
59178
+ };
59179
+ }
59180
+ var arraySize = (input) => Array.isArray(input) ? input.length : -1;
59181
+ var mapSize = (input) => input instanceof Map ? input.size : -1;
59182
+ var setSize = (input) => input instanceof Set ? input.size : -1;
58971
59183
  function handleArrayResult(result, final, index) {
58972
59184
  if (result.issues.length) {
58973
59185
  final.issues.push(...prefixIssues(index, result.issues));
@@ -59011,6 +59223,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
59011
59223
  }
59012
59224
  return payload;
59013
59225
  };
59226
+ capSize(inst, arraySize);
59014
59227
  });
59015
59228
  function handlePropertyResult(result, final, key, input, optin, optout) {
59016
59229
  const isPresent = key in input;
@@ -59965,6 +60178,7 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
59965
60178
  return Promise.all(proms).then(() => payload);
59966
60179
  return payload;
59967
60180
  };
60181
+ capSize(inst, mapSize);
59968
60182
  });
59969
60183
  function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
59970
60184
  if (keyResult.issues.length) {
@@ -60031,6 +60245,7 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
60031
60245
  return Promise.all(proms).then(() => payload);
60032
60246
  return payload;
60033
60247
  };
60248
+ capSize(inst, setSize);
60034
60249
  });
60035
60250
  function handleSetResult(result, final) {
60036
60251
  if (result.issues.length) {
@@ -69480,9 +69695,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
69480
69695
  doc.write(`}`);
69481
69696
  return outputVar;
69482
69697
  }
69698
+ function generateSizeGate(doc, schema, size) {
69699
+ for (const [max, check] of sizeCaps(schema._zod.def.checks)) {
69700
+ doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
69701
+ }
69702
+ }
69483
69703
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
69484
69704
  const def = schema._zod.def;
69485
69705
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
69706
+ generateSizeGate(doc, schema, `${accessor}.length`);
69486
69707
  const outputVar = buildsValue ? newVar(ctx) : null;
69487
69708
  const iVar = newVar(ctx);
69488
69709
  const elemVar = newVar(ctx);
@@ -69882,6 +70103,7 @@ function literalPropertyKey(ctx, key) {
69882
70103
  function generateMapCheck(doc, ctx, schema, accessor) {
69883
70104
  const def = schema._zod.def;
69884
70105
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
70106
+ generateSizeGate(doc, schema, `${accessor}.size`);
69885
70107
  const outputVar = newVar(ctx);
69886
70108
  const kVar = newVar(ctx);
69887
70109
  const valVar = newVar(ctx);
@@ -69898,6 +70120,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
69898
70120
  function generateSetCheck(doc, ctx, schema, accessor) {
69899
70121
  const def = schema._zod.def;
69900
70122
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
70123
+ generateSizeGate(doc, schema, `${accessor}.size`);
69901
70124
  const outputVar = newVar(ctx);
69902
70125
  const valVar = newVar(ctx);
69903
70126
  doc.write(`const ${outputVar} = new Set();`);
@@ -75065,6 +75288,21 @@ function preserveCustomPayload(schema, key, knownTags) {
75065
75288
  return output2;
75066
75289
  });
75067
75290
  }
75291
+ function defaultOnErrorOptionalStringTag(schema, key) {
75292
+ return exports_external.preprocess((value) => {
75293
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
75294
+ return value;
75295
+ }
75296
+ const record2 = value;
75297
+ const tag = record2[key];
75298
+ if (tag === undefined || tag === null || typeof tag === "string") {
75299
+ return value;
75300
+ }
75301
+ const normalized = { ...record2 };
75302
+ delete normalized[key];
75303
+ return normalized;
75304
+ }, schema);
75305
+ }
75068
75306
  function vecSkipError(itemSchema) {
75069
75307
  return exports_external.array(itemSchema.catch(skippedItem)).transform((items) => items.filter((item) => item !== skippedItem));
75070
75308
  }
@@ -75083,16 +75321,12 @@ var zWriteTextFileRequest = object({
75083
75321
  var zReadTextFileRequest = object({
75084
75322
  sessionId: zSessionId,
75085
75323
  path: string2(),
75086
- line: defaultOnError(int().gte(0).max(4294967295, {
75324
+ line: int().gte(0).max(4294967295, {
75087
75325
  error: "Invalid value: Expected uint32 to be <= 4294967295"
75088
- }).nullish(), () => {
75089
- return;
75090
- }),
75091
- limit: defaultOnError(int().gte(0).max(4294967295, {
75326
+ }).nullish(),
75327
+ limit: int().gte(0).max(4294967295, {
75092
75328
  error: "Invalid value: Expected uint32 to be <= 4294967295"
75093
- }).nullish(), () => {
75094
- return;
75095
- }),
75329
+ }).nullish(),
75096
75330
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75097
75331
  return;
75098
75332
  })
@@ -75343,14 +75577,10 @@ var zEnvVariable = object({
75343
75577
  var zCreateTerminalRequest = object({
75344
75578
  sessionId: zSessionId,
75345
75579
  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
- }),
75580
+ args: array(string2()).optional(),
75581
+ env: array(zEnvVariable).optional(),
75582
+ cwd: string2().nullish(),
75583
+ outputByteLimit: number2().nullish(),
75354
75584
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75355
75585
  return;
75356
75586
  })
@@ -75876,10 +76106,8 @@ var zAuthMethodTerminal = object({
75876
76106
  description: defaultOnError(string2().nullish(), () => {
75877
76107
  return;
75878
76108
  }),
75879
- args: defaultOnError(vecSkipError(string2()).optional(), () => []),
75880
- env: defaultOnError(record(string2(), string2()).optional(), () => {
75881
- return;
75882
- }),
76109
+ args: array(string2()).optional(),
76110
+ env: record(string2(), string2()).optional(),
75883
76111
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
75884
76112
  return;
75885
76113
  })
@@ -76577,17 +76805,61 @@ var zRunningStateUpdate = object({
76577
76805
  return;
76578
76806
  })
76579
76807
  });
76580
- var zIdleStateUpdate = object({
76581
- stopReason: defaultOnError(zStopReason.nullish(), () => {
76808
+ var zErrorStopReason = object({
76809
+ error: defaultOnError(zError.nullish(), () => {
76582
76810
  return;
76811
+ })
76812
+ });
76813
+ var zIdleStateUpdate = defaultOnErrorOptionalStringTag(preserveCustomPayload(intersection(union([
76814
+ object({
76815
+ stopReason: literal("end_turn")
76816
+ }),
76817
+ object({
76818
+ stopReason: literal("max_tokens")
76819
+ }),
76820
+ object({
76821
+ stopReason: literal("max_turn_requests")
76822
+ }),
76823
+ object({
76824
+ stopReason: literal("refusal")
76825
+ }),
76826
+ object({
76827
+ stopReason: literal("cancelled")
76583
76828
  }),
76829
+ zErrorStopReason.and(object({
76830
+ stopReason: literal("error")
76831
+ })),
76832
+ excludeKnownTags(object({
76833
+ stopReason: string2()
76834
+ }), "stopReason", [
76835
+ "cancelled",
76836
+ "end_turn",
76837
+ "error",
76838
+ "max_tokens",
76839
+ "max_turn_requests",
76840
+ "refusal"
76841
+ ]),
76842
+ object({
76843
+ stopReason: _null3().optional()
76844
+ })
76845
+ ]), object({
76584
76846
  usage: defaultOnError(zUsage.nullish(), () => {
76585
76847
  return;
76586
76848
  }),
76587
76849
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76588
76850
  return;
76851
+ }),
76852
+ stopReason: defaultOnError(string2().nullish(), () => {
76853
+ return;
76589
76854
  })
76590
- });
76855
+ })), "stopReason", [
76856
+ "cancelled",
76857
+ "end_turn",
76858
+ "error",
76859
+ "max_tokens",
76860
+ "max_turn_requests",
76861
+ "refusal"
76862
+ ]), "stopReason");
76591
76863
  var zRequiresActionStateUpdate = object({
76592
76864
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76593
76865
  return;
@@ -76970,16 +77242,16 @@ var zMcpServer = union([
76970
77242
  ]);
76971
77243
  var zNewSessionRequest = object({
76972
77244
  cwd: string2(),
76973
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
76974
- mcpServers: requiredDefaultOnError(vecSkipError(zMcpServer), () => []),
77245
+ additionalDirectories: array(string2()).optional(),
77246
+ mcpServers: array(zMcpServer),
76975
77247
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76976
77248
  return;
76977
77249
  })
76978
77250
  });
76979
77251
  var zLoadSessionRequest = object({
76980
- mcpServers: requiredDefaultOnError(vecSkipError(zMcpServer), () => []),
77252
+ mcpServers: array(zMcpServer),
76981
77253
  cwd: string2(),
76982
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77254
+ additionalDirectories: array(string2()).optional(),
76983
77255
  sessionId: zSessionId,
76984
77256
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
76985
77257
  return;
@@ -77001,8 +77273,8 @@ var zDeleteSessionRequest = object({
77001
77273
  var zForkSessionRequest = object({
77002
77274
  sessionId: zSessionId,
77003
77275
  cwd: string2(),
77004
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77005
- mcpServers: defaultOnError(vecSkipError(zMcpServer).optional(), () => []),
77276
+ additionalDirectories: array(string2()).optional(),
77277
+ mcpServers: array(zMcpServer).optional(),
77006
77278
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77007
77279
  return;
77008
77280
  })
@@ -77010,8 +77282,8 @@ var zForkSessionRequest = object({
77010
77282
  var zResumeSessionRequest = object({
77011
77283
  sessionId: zSessionId,
77012
77284
  cwd: string2(),
77013
- additionalDirectories: defaultOnError(vecSkipError(string2()).optional(), () => []),
77014
- mcpServers: defaultOnError(vecSkipError(zMcpServer).optional(), () => []),
77285
+ additionalDirectories: array(string2()).optional(),
77286
+ mcpServers: array(zMcpServer).optional(),
77015
77287
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77016
77288
  return;
77017
77289
  })
@@ -77392,7 +77664,7 @@ var zDidChangeDocumentNotification = object({
77392
77664
  sessionId: zSessionId,
77393
77665
  uri: string2(),
77394
77666
  version: number2(),
77395
- contentChanges: requiredDefaultOnError(vecSkipError(zTextDocumentContentChangeEvent), () => []),
77667
+ contentChanges: array(zTextDocumentContentChangeEvent),
77396
77668
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
77397
77669
  return;
77398
77670
  })
@@ -78471,9 +78743,26 @@ var zGuardElicitationPropertySchemaBoolean = zBooleanPropertySchema.and(object({
78471
78743
  var zGuardElicitationPropertySchemaArray = zMultiSelectPropertySchema.and(object({ type: literal("array") }));
78472
78744
  var zGuardMultiSelectItemsString = zStringMultiSelectItems.and(object({ type: literal("string") }));
78473
78745
  var zGuardStateUpdateRunning = zRunningStateUpdate.and(object({ state: literal("running") }));
78474
- var zGuardStateUpdateIdle = zIdleStateUpdate.and(object({ state: literal("idle") }));
78746
+ var zGuardStateUpdateIdle = zIdleStateUpdate.and(object({ usage: zUsage.nullish() })).and(object({ state: literal("idle") }));
78475
78747
  var zGuardStateUpdateRequiresAction = zRequiresActionStateUpdate.and(object({ state: literal("requires_action") }));
78476
78748
  var zGuardStateUpdateUnknown = zUnknownStateUpdate.and(object({ state: literal("unknown") }));
78749
+ var zGuardIdleStateUpdateEndTurn = object({
78750
+ stopReason: literal("end_turn")
78751
+ });
78752
+ var zGuardIdleStateUpdateMaxTokens = object({
78753
+ stopReason: literal("max_tokens")
78754
+ });
78755
+ var zGuardIdleStateUpdateMaxTurnRequests = object({
78756
+ stopReason: literal("max_turn_requests")
78757
+ });
78758
+ var zGuardIdleStateUpdateRefusal = object({
78759
+ stopReason: literal("refusal")
78760
+ });
78761
+ var zGuardIdleStateUpdateCancelled = object({
78762
+ stopReason: literal("cancelled")
78763
+ });
78764
+ var zGuardIdleStateUpdateError = zErrorStopReason.and(object({ error: zError.nullish() })).and(object({ stopReason: literal("error") }));
78765
+ var zGuardIdleStateUpdateNone = object({ stopReason: _null3().optional() });
78477
78766
  var zGuardCreateElicitationResponseAccept = zElicitationAcceptAction.and(object({ action: literal("accept") }));
78478
78767
  var zGuardCreateElicitationResponseDecline = object({
78479
78768
  action: literal("decline")
@@ -79832,6 +80121,7 @@ async function runAcp(apiUrl, apiKey, opts) {
79832
80121
 
79833
80122
  // src/shutdown.ts
79834
80123
  var activeRun2 = null;
80124
+ var stopOnExit2 = false;
79835
80125
  var cleanups2 = [];
79836
80126
  var exiting2 = null;
79837
80127
  var cancelling2 = null;
@@ -79839,6 +80129,11 @@ var CANCEL_TIMEOUT_MS2 = 5000;
79839
80129
  function setActiveRun2(run2) {
79840
80130
  activeRun2 = run2;
79841
80131
  }
80132
+ function stopRunOnExit() {
80133
+ stopOnExit2 = true;
80134
+ }
80135
+ var detachNotice2 = (todoId) => `Todo keeps running — reattach: tfa-cli --resume ${todoId}
80136
+ `;
79842
80137
  function trackStart(ws, projectId, started) {
79843
80138
  setActiveRun2(started.then((todoId) => ({ ws, projectId, todoId }), () => null));
79844
80139
  }
@@ -79856,7 +80151,7 @@ function cancelActiveRun2(timeoutMs = CANCEL_TIMEOUT_MS2) {
79856
80151
  if (cancelling2)
79857
80152
  return cancelling2;
79858
80153
  const pending = activeRun2;
79859
- if (!pending)
80154
+ if (!pending || !stopOnExit2)
79860
80155
  return Promise.resolve({ ok: false, todoId: null });
79861
80156
  const res = { ok: false };
79862
80157
  const attempt = (async () => {
@@ -79882,7 +80177,11 @@ function shutdown2(code, reason) {
79882
80177
  process.stderr.write(`
79883
80178
  ${reason}
79884
80179
  `);
79885
- if (activeRun2) {
80180
+ if (activeRun2 && !stopOnExit2) {
80181
+ const run2 = await withTimeout2(Promise.resolve(activeRun2), CANCEL_TIMEOUT_MS2, null);
80182
+ if (run2)
80183
+ process.stderr.write(detachNotice2(run2.todoId));
80184
+ } else if (activeRun2) {
79886
80185
  const { ok, todoId } = await cancelActiveRun2();
79887
80186
  if (ok)
79888
80187
  process.stderr.write(`Stopped todo ${todoId}
@@ -79912,7 +80211,7 @@ function finish(code) {
79912
80211
  });
79913
80212
  }
79914
80213
  function installSignalHandlers() {
79915
- process.on("SIGINT", () => void shutdown2(130, "Cancelled by user (Ctrl+C)"));
80214
+ process.on("SIGINT", () => void shutdown2(130, "Exited (Ctrl+C)"));
79916
80215
  process.on("SIGTERM", () => void shutdown2(143, "Terminated (SIGTERM)"));
79917
80216
  process.on("SIGHUP", () => void shutdown2(129, "Hangup (SIGHUP)"));
79918
80217
  }
@@ -80632,8 +80931,7 @@ async function main() {
80632
80931
  await watchTodo(ws2, todoId, projectId2, {
80633
80932
  json: !!args.json,
80634
80933
  autoApprove,
80635
- agentSettings: agent3,
80636
- exitOnInterrupt: !!args["non-interactive"]
80934
+ agentSettings: agent3
80637
80935
  });
80638
80936
  if (!args["non-interactive"]) {
80639
80937
  process.stderr.write(`
@@ -80684,7 +80982,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80684
80982
  cfg.addToHistory(followUp);
80685
80983
  await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
80686
80984
  await linkToSpawningBlock(api2, todoId);
80687
- await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3, exitOnInterrupt: !!args["non-interactive"] });
80985
+ await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3 });
80688
80986
  }
80689
80987
  if (!args["non-interactive"]) {
80690
80988
  await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
@@ -80706,6 +81004,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80706
81004
  if (mayfly) {
80707
81005
  process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
80708
81006
  `);
81007
+ stopRunOnExit();
80709
81008
  onShutdown(mayfly.stop);
80710
81009
  mayfly.child.on("exit", (code, sig) => {
80711
81010
  if (!mayfly.stopped())
@@ -80845,8 +81144,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
80845
81144
  await watchTodo(ws, actualTodoId, projectId, {
80846
81145
  json: !!args.json,
80847
81146
  autoApprove,
80848
- agentSettings: agent2,
80849
- exitOnInterrupt: !!args["non-interactive"]
81147
+ agentSettings: agent2
80850
81148
  });
80851
81149
  if (!args["non-interactive"]) {
80852
81150
  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.67",
10
10
  "type": "module",
11
11
  "bin": {
12
12
  "todoforai-cli": "bin/todoforai-cli.js",