@todoforai/cli 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/todoforai-cli.js +422 -104
  2. package/package.json +1 -1
@@ -42905,8 +42905,8 @@ class ApiClient {
42905
42905
  removeVoiceSource(businessContextId, channel) {
42906
42906
  return this.request("POST", "/api/v1/business-context/remove-voice-source", { businessContextId, channel });
42907
42907
  }
42908
- refineBrandVoice(businessContextId, answers, source) {
42909
- return this.request("POST", "/api/v1/business-context/refine-brand-voice", { businessContextId, answers, ...source && { source } });
42908
+ refineBrandVoice(businessContextId, answers, source, correction) {
42909
+ return this.request("POST", "/api/v1/business-context/refine-brand-voice", { businessContextId, answers, ...source && { source }, ...correction && { correction } });
42910
42910
  }
42911
42911
  getProfile() {
42912
42912
  return this.request("GET", "/api/v1/users/profile");
@@ -43729,6 +43729,9 @@ var SERVER_TO_FRONTENDS = {
43729
43729
  hexGrid: {
43730
43730
  updated: (projectId) => `${"hexgrid:updated" /* HEXGRID_UPDATED */}:${projectId}`
43731
43731
  },
43732
+ killSwitch: {
43733
+ changed: (projectId) => `${"project:kill_switch" /* KILL_SWITCH */}:${projectId}`
43734
+ },
43732
43735
  sandbox: {
43733
43736
  state: (userId) => `${"sandbox:state" /* SANDBOX_STATE */}:${userId}`
43734
43737
  },
@@ -44390,6 +44393,10 @@ var TOPICS = {
44390
44393
  channel: (p) => SERVER_TO_FRONTENDS.hexGrid.updated(p.projectId),
44391
44394
  audience: "frontend"
44392
44395
  },
44396
+ ["project:kill_switch" /* KILL_SWITCH */]: {
44397
+ channel: (p) => SERVER_TO_FRONTENDS.killSwitch.changed(p.projectId),
44398
+ audience: "frontend"
44399
+ },
44393
44400
  ["sandbox:state" /* SANDBOX_STATE */]: {
44394
44401
  channel: (p) => SERVER_TO_FRONTENDS.sandbox.state(p.userId),
44395
44402
  audience: "frontend"
@@ -44526,7 +44533,7 @@ import { parseArgs } from "util";
44526
44533
  // package.json
44527
44534
  var package_default = {
44528
44535
  name: "@todoforai/cli",
44529
- version: "0.1.48",
44536
+ version: "0.1.50",
44530
44537
  type: "module",
44531
44538
  bin: {
44532
44539
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44568,45 +44575,35 @@ function getEnv(name) {
44568
44575
  }
44569
44576
  function printUsage() {
44570
44577
  process.stderr.write(`
44571
- tfa-cli — TODOforAI CLI. Legacy aliases: todoforai-cli, todoai.
44578
+ tfa-cli — TODOforAI CLI
44572
44579
 
44573
- Usage:
44580
+ Usage: tfa-cli [OPTIONS] ["prompt"]
44581
+ tfa-cli [OPTIONS] <COMMAND> [ARGS]
44582
+
44583
+ Examples:
44574
44584
  tfa-cli login # Browser-based device auth
44575
- tfa-cli "prompt text" # Prompt as argument
44576
- tfa-cli -n "Quick task" # Non-interactive (run and exit)
44577
- echo "content" | tfa-cli # Pipe from stdin
44578
- tfa-cli --path /my/project "Fix bug" # Explicit workspace path
44579
- tfa-cli -c ["prompt"] # Resume last todo (optional prompt sent on attach)
44580
- tfa-cli --resume <todo-id> ["prompt"] # Resume specific todo (optional prompt sent on attach)
44581
- tfa-cli --inspect <todo-id>[@<slice>] # Read chat log. <slice> = -3:, :1, 5:10, 7 (Python-style)
44582
- tfa-cli start <id> # Start a TODO from the registry (todoregistry.com)
44583
- tfa-cli acp # Agent Client Protocol server on stdio (Zed, JetBrains, …; see README)
44584
- tfa-cli --list-agents # List available agents and exit
44585
- tfa-cli --list-models [filter] # List models usable with --model and exit
44586
- tfa-cli agent update <agent> model=<model> # Update agent settings (see 'agent --help'; also 'agent create')
44587
- tfa-cli todo set <todo-id|-> title=… group=… star=true # Edit a todo's fields (see 'todo --help')
44588
- tfa-cli project set|settings|groups|default|agent … # Edit the project, groups, defaults (see 'project --help')
44589
- tfa-cli brand list|create|select|voice … # Brands (business contexts) + voice learning (see 'brand --help')
44590
- tfa-cli device list|rename|wallpaper … # Paired machines (see 'device --help')
44591
- tfa-cli list [-n 30] [--cursor N] [--all] [--status S] # List todos (paginated); see 'list --help'
44592
- tfa-cli status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
44585
+ tfa-cli "prompt text" # New todo
44586
+ echo "content" | tfa-cli # From stdin
44587
+ tfa-cli start <id> # Registry template
44588
+ tfa-cli acp # Agent Client Protocol (stdio)
44589
+ tfa-cli agents # List agents
44590
+ tfa-cli models [filter] # Available models
44591
+ tfa-cli agent list|get|update|create # Agents (update <agent> model=…)
44592
+ tfa-cli todo set <todo-id|-> title=… group=… star=true # Edit a todo's fields
44593
+ tfa-cli project list|set|settings|groups|default|agent # Projects; edit the current one
44594
+ tfa-cli brand list|create|select|voice … # Brands (business contexts) + voice learning
44595
+ tfa-cli device list|rename|wallpaper # Paired machines
44596
+ tfa-cli list [-n 30] [--cursor N] [--all] [--status S] # List todos (paginated)
44597
+ tfa-cli status <todo-id> <STATUS> # Update a todo's status
44593
44598
  tfa-cli delete <todo-id> # Permanently delete a todo
44594
- tfa-cli addmessage <todo-id> "text" # Add a message to an existing todo
44595
- tfa-cli show <file|-> [todo-id] # Show a file in the chat (rendered by mimetype; - reads stdin)
44596
- # [--title T] [--alias A] [--mime M] [--card <name>] [--link] [--json]
44597
- # --link = compact chip (name+size+download) instead of inline render
44598
- # Prints "<todoId>:<alias|id> <public url>". Re-showing with the same
44599
- # --alias updates that block in place and pushes a new version to the
44600
- # same url (older versions stay reachable at /<id>/<version>).
44601
- tfa-cli show rm <ref|alias> # Take a shown file down (block, url, every version)
44602
- tfa-cli show list [todo-id] # List show blocks (ref, title, mime/url, card)
44603
- # [--project <id>] [--card <name>] [--json]
44604
- # no todo-id + --project (or $TODOFORAI_PROJECT_ID) = every todo
44605
- tfa-cli open <url> [todo-id] # Show a live http(s) url in the chat as a preview
44606
- # [--title T] [--alias A] [--json]
44607
- tfa-cli recommend --template <id> # Add a template as a recommendation card (see 'todoregistry-cli create')
44608
- tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> ownership links for a project you own
44609
- tfa-cli next [--direction "<text>"] # Ask the analyzer for growth recommendation cards (optional free-text steer)
44599
+ tfa-cli addmessage <todo-id> "text" # Send a message and exit (like -r, no watch/bridge)
44600
+ tfa-cli show <file|-> [todo-id] # Render a local file (image/pdf/html…) in the chat; - = stdin
44601
+ tfa-cli show list|rm … # List / take down shown files
44602
+ tfa-cli open <url> [todo-id] # Live url preview in the chat
44603
+ tfa-cli recommend --template <id> # Add a template as a recommendation card
44604
+ tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> links for a project you own
44605
+ tfa-cli next [--direction "<text>"] # Analyzer growth recommendation cards
44606
+ tfa-cli <command> help # Details per command
44610
44607
 
44611
44608
  Options:
44612
44609
  --path <dir> Workspace path (default: cwd)
@@ -44614,36 +44611,46 @@ Options:
44614
44611
  --agent, -a <name> Agent name (partial match)
44615
44612
  --group <slug> Group new todo; omitted inherits TODOFORAI_GROUP_ID
44616
44613
  --group-name <name> Display name for --group (last write wins)
44617
- --model <model> Override the agent's model for this todo
44618
- (e.g. anthropic:anthropic/claude-opus-5, openai:openai/gpt-5.6-sol)
44619
- --list-agents List available agents (name, id, workspace paths) and exit
44620
- --list-models List models usable with --model (optional substring filter) and exit
44621
- --api-url <url> API URL
44622
- --api-key <key> API key
44614
+ --model <model> Model for this todo only (see 'models')
44615
+ --api-url <url>
44616
+ --api-key <key>
44623
44617
  --user-id <id> Admin HTTP impersonation; requires --no-watch
44624
- --inspect, -i <todo-id>[@<slice>] Print chat log (read-only)
44625
- --template, -t <id> ["prompt"] Start from a registry template (alias: start <id>); prompt overrides the template task
44626
- --resume, -r [todo-id] Resume existing todo
44627
- --continue, -c Continue most recent todo
44628
- --non-interactive, -n Run to completion and exit without interactive prompt
44629
- --dangerously-skip-permissions Auto-approve all blocks (for CI/benchmarks)
44630
- --allow-all Set permissions to allow all tools (no approval needed)
44618
+ --inspect, -i <todo-id>[@<slice>] Read chat log; slices: -3:, :1, 5:10, 7
44619
+ --template, -t <id> ["prompt"] Same as start; prompt overrides template task
44620
+ --resume, -r <todo-id> ["prompt"] Resume; optional follow-up
44621
+ --continue, -c ["prompt"] Resume last; optional follow-up
44622
+ --non-interactive, -n Run to completion, then exit
44623
+ --dangerously-skip-permissions CLI auto-approves every block
44624
+ --allow-all Agent permissions allow:*:* nothing ever asks
44631
44625
  --raw-sysmsg <file> Use file contents verbatim as system prompt (new TODO only)
44632
44626
  --no-watch Create todo and exit
44633
- --isolated Spawn an ephemeral, task-scoped bridge: the agent sees ONLY
44634
- this machine (workspace = --path/cwd); session dies with the CLI
44627
+ --isolated Agent sees ONLY this machine + dir (no cloud VM/other devices); ends with CLI
44635
44628
  --no-bridge Do not auto-spawn bridge
44636
- --no-edge Deprecated alias for --no-bridge
44637
- --json Output as JSON
44638
- --detailed 'inspect --json': keep ids, timestamps, agentSettingsId, scheduledTimestamp
44639
- --format-anthropic 'inspect --json': Anthropic-style shape (tool_result in next user msg); attachment sources are uri-typed, so not a 1:1 messages.create input
44629
+ --json
44630
+ --detailed | --format-anthropic inspect --json: keep ids/timestamps | Anthropic messages shape
44640
44631
  --safe Validate API key upfront
44641
- --debug, -d Debug output
44632
+ --debug, -d
44642
44633
  --debug-dump Attach LLM request debug info per turn (requires server grant)
44643
- --show-config Show config
44644
- --reset-config Reset config file
44645
- --version, -v Print version and exit
44646
- --help, -h Show this help
44634
+ help | version | config = -h | -v | --show-config (--reset-config wipes it)
44635
+ `);
44636
+ }
44637
+ function printShowHelp() {
44638
+ process.stderr.write(`
44639
+ tfa-cli show — put a file or url into the chat
44640
+
44641
+ Usage:
44642
+ tfa-cli show <file|-> [todo-id] Render a file by mimetype (- reads stdin)
44643
+ [--title T] [--alias A] [--mime M] [--card <name>] [--link] [--json]
44644
+ --link: compact download chip instead of inline render
44645
+ Prints "<todoId>:<alias|id> <public url>". Re-showing with the same
44646
+ --alias updates the block in place; old versions stay at /<id>/<version>
44647
+ tfa-cli show rm <ref|alias> Take it down (block, url, every version)
44648
+ tfa-cli show list [todo-id] List show blocks (ref, title, mime/url, card)
44649
+ [--project <id>] [--card <name>] [--json]
44650
+ no todo-id + --project (or $TODOFORAI_PROJECT_ID) = every todo
44651
+ tfa-cli open <url> [todo-id] Live http(s) url as a preview [--title T] [--alias A] [--json]
44652
+
44653
+ todo-id defaults to $TODOFORAI_TODO_ID (agent shell) or the last todo this CLI touched.
44647
44654
  `);
44648
44655
  }
44649
44656
  var STATUS_HELP = {
@@ -44667,6 +44674,14 @@ All valid statuses:
44667
44674
  ${Object.values(TodoStatus).join(", ")}
44668
44675
  `);
44669
44676
  }
44677
+ var WORD_FLAGS = {
44678
+ help: "help",
44679
+ version: "version",
44680
+ models: "list-models",
44681
+ agents: "list-agents",
44682
+ config: "show-config"
44683
+ };
44684
+ var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open"];
44670
44685
  function parseCliArgs() {
44671
44686
  const { values, positionals } = parseArgs({
44672
44687
  args: process.argv.slice(2),
@@ -44698,6 +44713,8 @@ function parseCliArgs() {
44698
44713
  url: { type: "string" },
44699
44714
  "pasted-file": { type: "string" },
44700
44715
  "from-company": { type: "boolean", default: false },
44716
+ "dry-run": { type: "boolean", default: false },
44717
+ max: { type: "string" },
44701
44718
  seed: { type: "string" },
44702
44719
  emails: { type: "string" },
44703
44720
  ttl: { type: "string" },
@@ -44728,6 +44745,15 @@ function parseCliArgs() {
44728
44745
  });
44729
44746
  if (values["no-edge"])
44730
44747
  values["no-bridge"] = true;
44748
+ const asFlag = !values["list-models"] && !values["list-agents"] && !values["show-config"] ? WORD_FLAGS[positionals[0]] : undefined;
44749
+ if (asFlag) {
44750
+ values[asFlag] = true;
44751
+ positionals.shift();
44752
+ }
44753
+ if (positionals[1] === "help" && HELP_SUBCOMMANDS.includes(positionals[0])) {
44754
+ values.help = true;
44755
+ positionals.splice(1, 1);
44756
+ }
44731
44757
  return { values, positionals };
44732
44758
  }
44733
44759
 
@@ -47608,6 +47634,180 @@ var STATUS_HELP2 = {
47608
47634
  ["DELETED" /* DELETED */]: "Marked for deletion"
47609
47635
  };
47610
47636
 
47637
+ // src/voice-collect.ts
47638
+ import { execFileSync } from "node:child_process";
47639
+ var run = (bin, args) => execFileSync(bin, args, { encoding: "utf8", maxBuffer: 256 << 20, timeout: 60000, stdio: ["ignore", "pipe", "ignore"] });
47640
+ var installed = (bin) => {
47641
+ try {
47642
+ run("which", [bin]);
47643
+ return true;
47644
+ } catch {
47645
+ return false;
47646
+ }
47647
+ };
47648
+ var MIN_CHARS = 30;
47649
+ var SEP = /^─{20,}$/m;
47650
+ function parseThread(out) {
47651
+ return out.split(SEP).slice(1).map((chunk) => {
47652
+ const lines = chunk.trim().split(`
47653
+ `);
47654
+ let i = 0;
47655
+ const h = {};
47656
+ for (;i < lines.length && /^\s*(From|To|Date|Auth|Cc):/.test(lines[i]); i++) {
47657
+ const [k, ...v] = lines[i].trim().split(":");
47658
+ h[k] = v.join(":").trim();
47659
+ }
47660
+ return { from: h.From ?? "", date: h.Date ?? "", body: lines.slice(i).join(`
47661
+ `).trim() };
47662
+ }).filter((m) => m.from);
47663
+ }
47664
+ function stripReply(body) {
47665
+ const lines = body.split(`
47666
+ `);
47667
+ const cut = lines.findIndex((l, i) => /^>/.test(l) || /^(On .{6,}wrote:|.{3,} ezt írta \(időpont:|Le .{6,} a écrit :|Am .{6,} schrieb .*:|-----Original Message-----|From: .*|________________________________)$/i.test(l.trim()) || /^(On .{6,}|.{3,} ezt írta)/.test(l.trim()) && i + 1 < lines.length && /^>|wrote:$|\):$/.test(lines[i + 1].trim()));
47668
+ let kept = (cut >= 0 ? lines.slice(0, cut) : lines).filter((l) => !/^Attachments: /.test(l)).join(`
47669
+ `).trim();
47670
+ kept = kept.replace(/\n--\s*\n[\s\S]*$/, "").trim();
47671
+ return kept;
47672
+ }
47673
+ var gmail = {
47674
+ tool: "zele",
47675
+ check: () => {
47676
+ if (!installed("zele"))
47677
+ return "zele is not installed";
47678
+ const who = (() => {
47679
+ try {
47680
+ return run("zele", ["whoami"]);
47681
+ } catch {
47682
+ return "";
47683
+ }
47684
+ })();
47685
+ return /email:/.test(who) ? null : "zele is not signed in (run: zele login --method google)";
47686
+ },
47687
+ collect: (max) => {
47688
+ const accounts = [...run("zele", ["whoami"]).matchAll(/email: (\S+)/g)].map((m) => m[1]);
47689
+ const isMe = (from) => accounts.some((a) => from.toLowerCase().includes(a.toLowerCase()));
47690
+ const samples = [];
47691
+ for (const account of accounts) {
47692
+ const list = run("zele", ["mail", "search", "from:me", "--limit", String(max), "--account", account]);
47693
+ const ids = [...list.matchAll(/^ - id: ([0-9a-f]+)\n(?:.*\n)*? messages: (\d+)/gm)].filter((m) => Number(m[2]) > 1).map((m) => m[1]);
47694
+ for (const id of ids) {
47695
+ if (samples.length >= max)
47696
+ break;
47697
+ const msgs = parseThread(run("zele", ["mail", "read", id, "--account", account]));
47698
+ for (let i = 1;i < msgs.length && samples.length < max; i++) {
47699
+ if (!isMe(msgs[i].from) || isMe(msgs[i - 1].from))
47700
+ continue;
47701
+ const text = stripReply(msgs[i].body), prompt = stripReply(msgs[i - 1].body);
47702
+ if (text.length < MIN_CHARS || prompt.length < 20)
47703
+ continue;
47704
+ samples.push({ at: msgs[i].date, text, prompt, url: `gmail:${account}/${id}#${i}` });
47705
+ }
47706
+ }
47707
+ }
47708
+ return { samples, note: `${accounts.length} account(s)` };
47709
+ }
47710
+ };
47711
+ var outlook = {
47712
+ tool: "outlook-api",
47713
+ check: () => {
47714
+ if (!installed("outlook-api"))
47715
+ return "outlook-api is not installed";
47716
+ try {
47717
+ const me = JSON.parse(run("outlook-api", ["whoami"]));
47718
+ return me?.mail || me?.userPrincipalName ? null : "outlook-api is not signed in";
47719
+ } catch {
47720
+ return "outlook-api is not signed in (run: outlook-api auth)";
47721
+ }
47722
+ },
47723
+ collect: (max) => {
47724
+ const me = JSON.parse(run("outlook-api", ["whoami"]));
47725
+ const mine = [me.mail, me.userPrincipalName].filter(Boolean).map((a) => a.toLowerCase());
47726
+ const isMe = (m) => mine.includes((m.from?.emailAddress?.address ?? "").toLowerCase());
47727
+ const sent = JSON.parse(run("outlook-api", ["list", "-f", "sentitems", "-n", String(max), "--select", "id,conversationId,from,sentDateTime,webLink"]));
47728
+ const sentRows = Array.isArray(sent) ? sent : sent.value ?? [];
47729
+ const samples = [];
47730
+ const seenConv = new Set;
47731
+ for (const s of sentRows) {
47732
+ if (!s.conversationId || seenConv.has(s.conversationId))
47733
+ continue;
47734
+ seenConv.add(s.conversationId);
47735
+ const conv = JSON.parse(run("outlook-api", ["get", `/me/messages?$filter=conversationId eq '${s.conversationId.replace(/'/g, "''")}'&$top=50&$select=id,from,receivedDateTime,sentDateTime,webLink`]));
47736
+ const msgs = (conv.value ?? []).sort((a, b) => (a.receivedDateTime ?? "").localeCompare(b.receivedDateTime ?? ""));
47737
+ const body = (id) => stripReply(JSON.parse(run("outlook-api", ["read", id, "--text"])).body?.content ?? "");
47738
+ for (let i = 1;i < msgs.length && samples.length < max; i++) {
47739
+ if (!isMe(msgs[i]) || isMe(msgs[i - 1]))
47740
+ continue;
47741
+ const text = body(msgs[i].id), prompt = body(msgs[i - 1].id);
47742
+ if (text.length < MIN_CHARS || prompt.length < 20)
47743
+ continue;
47744
+ samples.push({ at: msgs[i].sentDateTime ?? msgs[i].receivedDateTime, text, prompt, url: msgs[i].webLink ?? `outlook:${msgs[i].id}` });
47745
+ }
47746
+ if (samples.length >= max)
47747
+ break;
47748
+ }
47749
+ return { samples };
47750
+ }
47751
+ };
47752
+ var QUERIES = "the we you ok this that fix why do how make add run test build post code file error need want should could now more less good bad new old first last".split(" ");
47753
+ var chat = {
47754
+ tool: "tfa-memory",
47755
+ check: () => installed("tfa-memory") ? null : "tfa-memory is not installed",
47756
+ collect: (max) => {
47757
+ const mem = (...a) => run("tfa-memory", a);
47758
+ const anchors = new Map;
47759
+ for (const q of QUERIES) {
47760
+ for (const line of mem("search", q, "--source", "todo", "--mode", "lexical", "--limit", "50").split(`
47761
+ `)) {
47762
+ const m = line.match(/todo\/([0-9a-f-]{36}):([0-9a-f-]{36})/);
47763
+ if (m && !anchors.has(m[1]))
47764
+ anchors.set(m[1], `todo/${m[1]}:${m[2]}`);
47765
+ }
47766
+ if (anchors.size >= max)
47767
+ break;
47768
+ }
47769
+ const samples = [];
47770
+ for (const anchor of [...anchors.values()].slice(0, max)) {
47771
+ let msgs;
47772
+ try {
47773
+ msgs = JSON.parse(mem("around", anchor, "--before", "200", "--after", "200", "--json"));
47774
+ } catch {
47775
+ continue;
47776
+ }
47777
+ msgs.sort((a, b) => a.createdAt - b.createdAt);
47778
+ for (let i = 1;i < msgs.length; i++) {
47779
+ if (msgs[i].meta?.role !== "user" || msgs[i - 1].meta?.role === "user")
47780
+ continue;
47781
+ const text = msgs[i].content.trim(), prompt = msgs[i - 1].content.trim();
47782
+ if (text.length < MIN_CHARS || prompt.length < 40)
47783
+ continue;
47784
+ if (/^\s*(#|\/|@|\{)/.test(text))
47785
+ continue;
47786
+ if (text.split(`
47787
+ `).length > 6)
47788
+ continue;
47789
+ if (/^Wait w_|\nWait w_[0-9a-f]+ '|^Connected \w+ \(card\)|^\[/m.test(text))
47790
+ continue;
47791
+ samples.push({ at: new Date(msgs[i].createdAt).toISOString(), text, prompt: prompt.slice(0, 4000), url: msgs[i].address });
47792
+ }
47793
+ }
47794
+ return { samples: samples.slice(0, max), note: `${anchors.size} todos` };
47795
+ }
47796
+ };
47797
+ var ADAPTERS = { Gmail: gmail, Outlook: outlook, Chat: chat };
47798
+ var DEVICE_CHANNELS = Object.keys(ADAPTERS);
47799
+ function checkChannel(channel) {
47800
+ const a = ADAPTERS[channel];
47801
+ return a ? a.check() : `${channel} is not read on the device (server-side: pass --url, or paste)`;
47802
+ }
47803
+ function collectChannel(channel, max = 40) {
47804
+ const a = ADAPTERS[channel];
47805
+ if (!a)
47806
+ throw new Error(`No device adapter for ${channel}`);
47807
+ const r = a.collect(Math.min(200, Math.max(1, Math.floor(Number(max) || 40))));
47808
+ return { ...r, samples: r.samples.filter((x) => x.text.trim().length >= MIN_CHARS) };
47809
+ }
47810
+
47611
47811
  // src/manage-command.ts
47612
47812
  function printTodoHelp() {
47613
47813
  process.stderr.write(`
@@ -47644,6 +47844,7 @@ tfa-cli project — edit the current project
47644
47844
  --project <id> or $TODOFORAI_PROJECT_ID selects the project.
47645
47845
 
47646
47846
  Usage:
47847
+ tfa-cli project list Projects you can access (* = default)
47647
47848
  tfa-cli project set <field=value>… name=… description=… brand=<businessContextId>
47648
47849
  (isPublic refused from an agent shell)
47649
47850
  tfa-cli project default Make it the project you land on at /
@@ -47725,6 +47926,19 @@ async function projectCommand(api, positionals, args) {
47725
47926
  printProjectHelp();
47726
47927
  process.exit(0);
47727
47928
  }
47929
+ if (sub === "list" || sub === "ls") {
47930
+ const projects = await api.listProjects();
47931
+ if (args.json) {
47932
+ console.log(JSON.stringify(projects, null, 2));
47933
+ return;
47934
+ }
47935
+ for (const p of projects) {
47936
+ const pr = p.project ?? p;
47937
+ process.stderr.write(`${pr.isDefault ? "*" : " "} ${pr.name ?? ""} ${DIM}${pr.id}${RESET}
47938
+ `);
47939
+ }
47940
+ return;
47941
+ }
47728
47942
  if (!projectId)
47729
47943
  fail("No project — pass --project <id> or set TODOFORAI_PROJECT_ID");
47730
47944
  if (sub === "set") {
@@ -47812,12 +48026,18 @@ Usage:
47812
48026
  tfa-cli brand select <brand|none> Active brand for the account
47813
48027
  tfa-cli brand voice Learned profile + sources
47814
48028
  tfa-cli brand voice answers [<q>=<a>…] Show / set the brand-voice answers (strings)
47815
- tfa-cli brand voice collect <channel> [--url U | --pasted-file <F|->]
47816
- Add a writing sample source
47817
- channels: X LinkedIn Facebook Instagram
47818
- Gmail Outlook Slack Teams
48029
+ tfa-cli brand voice collect <channel> [--url U | --pasted-file <F|-> | --max N]
48030
+ Add a writing sample source.
48031
+ On this device (signed-in CLI, reply pairs):
48032
+ Gmail (zele) Outlook (outlook-api) Chat (tfa-memory)
48033
+ Server-side (--url or paste):
48034
+ X LinkedIn Facebook Instagram Slack Teams
48035
+ --dry-run print the samples as JSONL, store nothing
48036
+ tfa-cli brand voice check [<channel>|--all] Can this device read the channel? tool, login, sample count
47819
48037
  tfa-cli brand voice remove <channel>
47820
48038
  tfa-cli brand voice learn [--from-company] Run the refinement loop, store the profile
48039
+ tfa-cli brand voice correct "<what is off>" Tell the learner what it got wrong; profile is updated
48040
+ in one pass and the correction is kept for every re-learn
47821
48041
 
47822
48042
  <brand> is an id or name (unique partial works). Voice subcommands use the
47823
48043
  selected brand unless --brand <id|name> is given (--pasted-file - reads stdin).
@@ -47896,8 +48116,61 @@ async function brandCommand(api, positionals, args) {
47896
48116
  return voiceCommand(api, rest, args);
47897
48117
  fail(`Unknown 'brand' subcommand: ${sub}`);
47898
48118
  }
48119
+ function dryCollect(channel, max) {
48120
+ const reason = checkChannel(channel);
48121
+ if (reason)
48122
+ fail(`${channel}: ${reason}`);
48123
+ const { samples, note } = collectChannel(channel, max);
48124
+ for (const x of samples)
48125
+ console.log(JSON.stringify(x));
48126
+ process.stderr.write(`${DIM}${samples.length} samples${note ? ` · ${note}` : ""} (not stored)${RESET}
48127
+ `);
48128
+ }
48129
+ async function voiceDeviceCommand(rest, args) {
48130
+ const [verb, ...vargs] = rest;
48131
+ if (verb === "check") {
48132
+ const channels3 = args.all || !vargs[0] ? DEVICE_CHANNELS : [vargs[0]];
48133
+ let failed = 0;
48134
+ const report = {};
48135
+ for (const ch of channels3) {
48136
+ const reason = checkChannel(ch);
48137
+ if (reason) {
48138
+ failed++;
48139
+ report[ch] = { ok: false, reason };
48140
+ continue;
48141
+ }
48142
+ try {
48143
+ const r = collectChannel(ch, Number(args.max ?? 10));
48144
+ const bad = r.samples.filter((x) => !x.text.trim() || ADAPTERS[ch] && !x.prompt).length;
48145
+ const ok = r.samples.length > 0 && bad === 0;
48146
+ if (!ok)
48147
+ failed++;
48148
+ report[ch] = { ok, samples: r.samples.length, ...r.note && { note: r.note }, ...bad ? { reason: `${bad} sample(s) without a prompt` } : r.samples.length ? {} : { reason: "no samples came back" } };
48149
+ } catch (e) {
48150
+ failed++;
48151
+ report[ch] = { ok: false, reason: e.message };
48152
+ }
48153
+ }
48154
+ if (args.json)
48155
+ console.log(JSON.stringify(report, null, 2));
48156
+ else
48157
+ for (const [ch, r] of Object.entries(report))
48158
+ process.stderr.write(`${r.ok ? GREEN + "✅" : RED + "❌"} ${ch}${RESET} ${DIM}${r.ok ? `${r.samples} samples${r.note ? ` · ${r.note}` : ""}` : r.reason}${RESET}
48159
+ `);
48160
+ process.exit(failed ? 1 : 0);
48161
+ }
48162
+ if (verb === "collect" && args["dry-run"]) {
48163
+ if (!vargs[0])
48164
+ fail("Usage: tfa-cli brand voice collect <channel> --dry-run [--max N]");
48165
+ dryCollect(vargs[0], Number(args.max ?? 40));
48166
+ return true;
48167
+ }
48168
+ return false;
48169
+ }
47899
48170
  async function voiceCommand(api, rest, args) {
47900
48171
  const [verb, ...vargs] = rest;
48172
+ if (verb === "collect" && !vargs[0])
48173
+ fail("Usage: tfa-cli brand voice collect <channel> [--url U | --pasted-file F|- | --max N] [--dry-run]");
47901
48174
  const onboarding = await api.getOnboarding();
47902
48175
  if (verb === "answers") {
47903
48176
  if (vargs.length) {
@@ -47946,13 +48219,26 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
47946
48219
  }
47947
48220
  if (verb === "collect") {
47948
48221
  const channel = vargs[0];
47949
- if (!channel)
47950
- fail("Usage: tfa-cli brand voice collect <channel> [--url U | --pasted-file F|-]");
47951
48222
  let pasted;
47952
48223
  if (args["pasted-file"]) {
47953
48224
  const { readFileSync: readFileSync3 } = await import("node:fs");
47954
48225
  pasted = readFileSync3(args["pasted-file"] === "-" ? 0 : args["pasted-file"], "utf8");
47955
48226
  }
48227
+ const onDevice = !pasted && !args.url && ADAPTERS[channel];
48228
+ if (onDevice) {
48229
+ const reason = checkChannel(channel);
48230
+ if (reason)
48231
+ fail(`${channel}: ${reason}`);
48232
+ process.stderr.write(`${DIM}reading ${channel} on this device…${RESET}
48233
+ `);
48234
+ const { samples, note } = collectChannel(channel, Number(args.max ?? 40));
48235
+ if (!samples.length)
48236
+ fail(`${channel}: nothing usable came back${note ? ` (${note})` : ""}`);
48237
+ const { sources: sources2 } = await api.collectVoiceSource(brand.id, channel, { samples });
48238
+ process.stderr.write(`${GREEN}✅ ${channel}: ${samples.length} reply pairs stored (${sources2.length} source(s))${RESET}
48239
+ `);
48240
+ return;
48241
+ }
47956
48242
  const { sources } = await api.collectVoiceSource(brand.id, channel, { url: args.url, pasted });
47957
48243
  process.stderr.write(`${GREEN}✅ ${channel} collected (${sources.length} source(s))${RESET}
47958
48244
  `);
@@ -47983,6 +48269,31 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
47983
48269
  }
47984
48270
  process.stderr.write(`${GREEN}✅ voice learned (match ${res.match}/100, source ${res.source})${RESET}
47985
48271
  ${res.profile}
48272
+ `);
48273
+ return;
48274
+ }
48275
+ if (verb === "correct") {
48276
+ const text = vargs.join(" ").trim();
48277
+ if (!text)
48278
+ fail('Usage: tfa-cli brand voice correct "<what is off>"');
48279
+ const stored = onboarding.voiceProfiles?.[brand.id];
48280
+ if (!stored?.profile)
48281
+ fail("No voice learned yet — 'tfa-cli brand voice learn' first");
48282
+ process.stderr.write(`${DIM}applying correction…${RESET}
48283
+ `);
48284
+ const res = await api.refineBrandVoice(brand.id, onboarding.styleAnswers ?? {}, undefined, text);
48285
+ if (!res.profile)
48286
+ fail("The correction pass returned nothing — try rewording it");
48287
+ const { [brand.id]: _stale, ...styleAiAnswers } = onboarding.styleAiAnswers ?? {};
48288
+ await api.patchOnboarding({ voiceProfiles: { ...onboarding.voiceProfiles ?? {}, [brand.id]: { ...res, updatedAt: Date.now() } }, styleAiAnswers });
48289
+ if (args.json) {
48290
+ console.log(JSON.stringify(res, null, 2));
48291
+ return;
48292
+ }
48293
+ const last = res.iterations[res.iterations.length - 1];
48294
+ process.stderr.write(`${GREEN}✅ voice corrected (match ${res.match}/100)${RESET}
48295
+ ${res.profile}
48296
+ ${DIM}sample: ${last?.sample ?? ""}${RESET}
47986
48297
  `);
47987
48298
  return;
47988
48299
  }
@@ -48255,10 +48566,10 @@ function probeBridge2(bin) {
48255
48566
  function hasBridge2() {
48256
48567
  if (probeBridge2(bridgeBin2))
48257
48568
  return true;
48258
- const installed = path3.join(INSTALL_PREFIX2, "todoforai-bridge");
48259
- if (!probeBridge2(installed))
48569
+ const installed2 = path3.join(INSTALL_PREFIX2, "todoforai-bridge");
48570
+ if (!probeBridge2(installed2))
48260
48571
  return false;
48261
- bridgeBin2 = installed;
48572
+ bridgeBin2 = installed2;
48262
48573
  return true;
48263
48574
  }
48264
48575
  function installBridge2() {
@@ -67726,7 +68037,7 @@ function visit(schema, fnOrHandlers) {
67726
68037
  return h ? h(node2, rewritten) : node2;
67727
68038
  };
67728
68039
  const cache = new Map;
67729
- function run(s) {
68040
+ function run2(s) {
67730
68041
  const cached2 = cache.get(s);
67731
68042
  if (cached2 === RESOLVING) {
67732
68043
  return new $ZodLazy({
@@ -67752,21 +68063,21 @@ function visit(schema, fnOrHandlers) {
67752
68063
  let changed = false;
67753
68064
  const newShape = {};
67754
68065
  for (const k of keys) {
67755
- const mapped = run(oldShape[k]);
68066
+ const mapped = run2(oldShape[k]);
67756
68067
  if (mapped !== oldShape[k])
67757
68068
  changed = true;
67758
68069
  newShape[k] = mapped;
67759
68070
  }
67760
68071
  let newCatchall = def.catchall;
67761
68072
  if (def.catchall) {
67762
- newCatchall = run(def.catchall);
68073
+ newCatchall = run2(def.catchall);
67763
68074
  if (newCatchall !== def.catchall)
67764
68075
  changed = true;
67765
68076
  }
67766
68077
  return changed ? clone(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
67767
68078
  }
67768
68079
  case "array": {
67769
- const mapped = run(def.element);
68080
+ const mapped = run2(def.element);
67770
68081
  return mapped === def.element ? s : clone(s, { ...def, element: mapped });
67771
68082
  }
67772
68083
  case "tuple": {
@@ -67774,14 +68085,14 @@ function visit(schema, fnOrHandlers) {
67774
68085
  let changed = false;
67775
68086
  const newItems = [];
67776
68087
  for (const item of oldItems) {
67777
- const mapped = run(item);
68088
+ const mapped = run2(item);
67778
68089
  if (mapped !== item)
67779
68090
  changed = true;
67780
68091
  newItems.push(mapped);
67781
68092
  }
67782
68093
  let newRest = def.rest;
67783
68094
  if (def.rest) {
67784
- newRest = run(def.rest);
68095
+ newRest = run2(def.rest);
67785
68096
  if (newRest !== def.rest)
67786
68097
  changed = true;
67787
68098
  }
@@ -67789,12 +68100,12 @@ function visit(schema, fnOrHandlers) {
67789
68100
  }
67790
68101
  case "record":
67791
68102
  case "map": {
67792
- const newKey = run(def.keyType);
67793
- const newVal = run(def.valueType);
68103
+ const newKey = run2(def.keyType);
68104
+ const newVal = run2(def.valueType);
67794
68105
  return newKey === def.keyType && newVal === def.valueType ? s : clone(s, { ...def, keyType: newKey, valueType: newVal });
67795
68106
  }
67796
68107
  case "set": {
67797
- const newVal = run(def.valueType);
68108
+ const newVal = run2(def.valueType);
67798
68109
  return newVal === def.valueType ? s : clone(s, { ...def, valueType: newVal });
67799
68110
  }
67800
68111
  case "union": {
@@ -67802,7 +68113,7 @@ function visit(schema, fnOrHandlers) {
67802
68113
  let changed = false;
67803
68114
  const newOptions = [];
67804
68115
  for (const opt of oldOptions) {
67805
- const mapped = run(opt);
68116
+ const mapped = run2(opt);
67806
68117
  if (mapped !== opt)
67807
68118
  changed = true;
67808
68119
  newOptions.push(mapped);
@@ -67810,8 +68121,8 @@ function visit(schema, fnOrHandlers) {
67810
68121
  return changed ? clone(s, { ...def, options: newOptions }) : s;
67811
68122
  }
67812
68123
  case "intersection": {
67813
- const newLeft = run(def.left);
67814
- const newRight = run(def.right);
68124
+ const newLeft = run2(def.left);
68125
+ const newRight = run2(def.right);
67815
68126
  return newLeft === def.left && newRight === def.right ? s : clone(s, { ...def, left: newLeft, right: newRight });
67816
68127
  }
67817
68128
  case "optional":
@@ -67823,23 +68134,23 @@ function visit(schema, fnOrHandlers) {
67823
68134
  case "nonoptional":
67824
68135
  case "promise":
67825
68136
  case "success": {
67826
- const newInner = run(def.innerType);
68137
+ const newInner = run2(def.innerType);
67827
68138
  return newInner === def.innerType ? s : clone(s, { ...def, innerType: newInner });
67828
68139
  }
67829
68140
  case "pipe": {
67830
- const newIn = run(def.in);
67831
- const newOut = run(def.out);
68141
+ const newIn = run2(def.in);
68142
+ const newOut = run2(def.out);
67832
68143
  return newIn === def.in && newOut === def.out ? s : clone(s, { ...def, in: newIn, out: newOut });
67833
68144
  }
67834
68145
  case "function": {
67835
- const newInput = run(def.input);
67836
- const newOutput = run(def.output);
68146
+ const newInput = run2(def.input);
68147
+ const newOutput = run2(def.output);
67837
68148
  return newInput === def.input && newOutput === def.output ? s : clone(s, { ...def, input: newInput, output: newOutput });
67838
68149
  }
67839
68150
  case "lazy": {
67840
68151
  const original = def.getter;
67841
68152
  const { _cachedInner, ...rest } = def;
67842
- return clone(s, { ...rest, getter: () => run(original()) });
68153
+ return clone(s, { ...rest, getter: () => run2(original()) });
67843
68154
  }
67844
68155
  case "template_literal":
67845
68156
  case "string":
@@ -67867,7 +68178,7 @@ function visit(schema, fnOrHandlers) {
67867
68178
  }
67868
68179
  }
67869
68180
  }
67870
- return run(schema);
68181
+ return run2(schema);
67871
68182
  }
67872
68183
 
67873
68184
  // node_modules/zod/v4/classic/deep-partial.js
@@ -72552,6 +72863,16 @@ function formatPathWithTilde(path5) {
72552
72863
  const home = homedir3();
72553
72864
  return path5.startsWith(home) ? path5.replace(home, "~") : path5;
72554
72865
  }
72866
+ function promptArg(words) {
72867
+ if (words.length === 0)
72868
+ return;
72869
+ if (words.length > 1 || !/\s/.test(words[0])) {
72870
+ process.stderr.write(`${RED3}Error: "${words.join(" ")}" is not a subcommand, and a prompt must be ONE quoted argument, e.g. tfa-cli "fix the login bug" (or pipe it on stdin). Run tfa-cli --help for subcommands.${RESET2}
72871
+ `);
72872
+ process.exit(2);
72873
+ }
72874
+ return words[0];
72875
+ }
72555
72876
  async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoApprove, cfg) {
72556
72877
  while (true) {
72557
72878
  try {
@@ -72653,6 +72974,10 @@ Cancelled by user (Ctrl+C)
72653
72974
  printStatusHelp();
72654
72975
  process.exit(0);
72655
72976
  }
72977
+ if ((positionals[0] === "show" || positionals[0] === "open") && args.help) {
72978
+ printShowHelp();
72979
+ process.exit(0);
72980
+ }
72656
72981
  if (positionals[0] === "agent" && args.help) {
72657
72982
  printAgentHelp();
72658
72983
  process.exit(0);
@@ -72724,6 +73049,8 @@ Cancelled by user (Ctrl+C)
72724
73049
  process.exit(2);
72725
73050
  }
72726
73051
  }
73052
+ if (positionals[0] === "brand" && positionals[1] === "voice" && await voiceDeviceCommand(positionals.slice(2), args))
73053
+ return;
72727
73054
  const deviceLogin = () => runDeviceLogin(apiUrl).catch((e) => {
72728
73055
  if (!(e instanceof DeviceLoginError))
72729
73056
  throw e;
@@ -73143,7 +73470,7 @@ Cancelled by user (Ctrl+C)
73143
73470
  process.exit(1);
73144
73471
  }
73145
73472
  const promptWords = positionals[0] === "start" ? positionals.slice(2) : positionals;
73146
- const content2 = promptWords.join(" ").trim();
73473
+ const content2 = (promptArg(promptWords) ?? "").trim();
73147
73474
  const todo2 = await api2.startFromSpec(projectId2, templateId, {
73148
73475
  ...content2 ? { content: content2 } : {},
73149
73476
  ...groupTag ? { groupTag } : {},
@@ -73218,7 +73545,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73218
73545
  const ws2 = new FrontendWebSocket(apiUrl, apiKey);
73219
73546
  await ws2.connect();
73220
73547
  const autoApprove = !!args["dangerously-skip-permissions"];
73221
- const followUp = positionals.length > 0 ? positionals.join(" ") : process.stdin.isTTY ? "" : await readStdin();
73548
+ const followUp = promptArg(positionals) ?? (process.stdin.isTTY ? "" : await readStdin());
73222
73549
  if (followUp) {
73223
73550
  cfg.addToHistory(followUp);
73224
73551
  await api2.addMessage(projectId2, followUp, agent3, todoId);
@@ -73236,11 +73563,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73236
73563
  `);
73237
73564
  process.exit(2);
73238
73565
  }
73239
- if (positionals.length === 1 && !/\s/.test(positionals[0])) {
73240
- process.stderr.write(`${RED3}Error: "${positionals[0]}" is not a subcommand, and a prompt needs more than one word (or pipe it on stdin).${RESET2}
73241
- `);
73242
- process.exit(2);
73243
- }
73566
+ const argPrompt = promptArg(positionals);
73244
73567
  const ws = args["no-watch"] ? null : new FrontendWebSocket(apiUrl, apiKey);
73245
73568
  const wsReady = ws ? ws.connect() : null;
73246
73569
  const bridgeReady = !args["no-bridge"] && !args["no-watch"] && !args.isolated ? ensureBridgeRunning2(apiUrl, apiKey) : null;
@@ -73300,12 +73623,7 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73300
73623
  }
73301
73624
  process.stderr.write(`${DIM2}Tip: ${randomTip()}${RESET2}
73302
73625
  `);
73303
- let content;
73304
- if (positionals.length > 0) {
73305
- content = positionals.join(" ");
73306
- } else {
73307
- content = await readStdin();
73308
- }
73626
+ const content = argPrompt ?? await readStdin();
73309
73627
  const envProjectId = getEnv("PROJECT_ID");
73310
73628
  const hasProject = args.project || envProjectId || cfgScope.data.default_project_id;
73311
73629
  const storedAgent = cfgScope.data.default_agent_settings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "bin/todoforai-cli.js",