@todoforai/cli 0.1.47 → 0.1.48

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.
@@ -42860,6 +42860,75 @@ class ApiClient {
42860
42860
  deleteTodo(todoId) {
42861
42861
  return this.request("DELETE", `/api/v1/todos/${todoId}`);
42862
42862
  }
42863
+ restoreTodo(todoId) {
42864
+ return this.request("POST", `/api/v1/todos/${todoId}/restore`, { todoId });
42865
+ }
42866
+ reorderAgentSettings(agentSettingsIds) {
42867
+ return this.request("PUT", "/api/v1/agents/reorder", { agentSettingsIds });
42868
+ }
42869
+ updateTodo(todoId, updates) {
42870
+ return this.request("PUT", `/api/v1/todos/${todoId}`, { todoId, ...updates });
42871
+ }
42872
+ updateScheduledMessage(todoId, messageId, agentSettingsId, updates) {
42873
+ return this.request("PATCH", `/api/v1/todos/${todoId}/messages/${messageId}`, { todoId, messageId, agentSettingsId, ...updates });
42874
+ }
42875
+ setDefaultProject(projectId) {
42876
+ return this.request("POST", "/api/v1/users/default-project", { projectId });
42877
+ }
42878
+ setProjectDefaultAgent(projectId, agentSettingsId) {
42879
+ return this.request("POST", "/api/v1/users/project-agent-mapping", { projectId, agentSettingsId });
42880
+ }
42881
+ renameDevice(id, name) {
42882
+ return this.request("PATCH", `/api/v1/devices/${id}/name`, { id, name });
42883
+ }
42884
+ setDeviceWallpaperMode(id, mode) {
42885
+ return this.request("POST", `/api/v1/devices/${id}/wallpaper-mode`, { id, mode });
42886
+ }
42887
+ listBusinessContexts() {
42888
+ return this.request("GET", "/api/v1/business-context");
42889
+ }
42890
+ createBusinessContext(name) {
42891
+ return this.request("POST", "/api/v1/business-context", { name });
42892
+ }
42893
+ renameBusinessContext(id, name) {
42894
+ return this.request("PUT", `/api/v1/business-context/${id}`, { id, name });
42895
+ }
42896
+ selectBusinessContext(contextId) {
42897
+ return this.request("POST", "/api/v1/business-context/select", { contextId });
42898
+ }
42899
+ listVoiceSources(businessContextId) {
42900
+ return this.request("GET", `/api/v1/business-context/voice-sources?id=${encodeURIComponent(businessContextId)}`);
42901
+ }
42902
+ collectVoiceSource(businessContextId, channel, opts) {
42903
+ return this.request("POST", "/api/v1/business-context/collect-voice-source", { businessContextId, channel, ...opts });
42904
+ }
42905
+ removeVoiceSource(businessContextId, channel) {
42906
+ return this.request("POST", "/api/v1/business-context/remove-voice-source", { businessContextId, channel });
42907
+ }
42908
+ refineBrandVoice(businessContextId, answers, source) {
42909
+ return this.request("POST", "/api/v1/business-context/refine-brand-voice", { businessContextId, answers, ...source && { source } });
42910
+ }
42911
+ getProfile() {
42912
+ return this.request("GET", "/api/v1/users/profile");
42913
+ }
42914
+ getOnboarding() {
42915
+ return this.request("GET", "/api/v1/onboarding");
42916
+ }
42917
+ patchOnboarding(patch) {
42918
+ return this.request("PUT", "/api/v1/onboarding", { patch });
42919
+ }
42920
+ updateProject(projectId, updates) {
42921
+ return this.request("PUT", `/api/v1/projects/${projectId}`, { projectId, ...updates });
42922
+ }
42923
+ listProjectGroups(projectId) {
42924
+ return this.request("GET", `/api/v1/projects/${projectId}/groups`);
42925
+ }
42926
+ upsertProjectGroup(projectId, slug, fields) {
42927
+ return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
42928
+ }
42929
+ reorderProjectGroups(projectId, slugs) {
42930
+ return this.request("PUT", `/api/v1/projects/${projectId}/groups-order`, { projectId, slugs });
42931
+ }
42863
42932
  listModels() {
42864
42933
  return this.request("GET", "/api/v1/models");
42865
42934
  }
@@ -43669,6 +43738,9 @@ var SERVER_TO_FRONTENDS = {
43669
43738
  notification: {
43670
43739
  changed: (userId) => `${"notification:changed" /* NOTIFICATION_CHANGED */}:${userId}`
43671
43740
  },
43741
+ frontend: {
43742
+ presence: (userId) => `${"frontend:presence" /* FRONTEND_PRESENCE */}:${userId}`
43743
+ },
43672
43744
  agent: {
43673
43745
  status: (userId) => `${"agent:status" /* AGENT_STATUS */}:${userId}`,
43674
43746
  mcpList: () => `${"agent:mcp_list" /* AGENT_MCP_LIST */}:ALL`
@@ -44334,6 +44406,10 @@ var TOPICS = {
44334
44406
  channel: (p) => SERVER_TO_FRONTENDS.notification.changed(p.userId),
44335
44407
  audience: "frontend"
44336
44408
  },
44409
+ ["frontend:presence" /* FRONTEND_PRESENCE */]: {
44410
+ channel: (p) => SERVER_TO_FRONTENDS.frontend.presence(p.userId),
44411
+ audience: "frontend"
44412
+ },
44337
44413
  ["NOTIFY" /* NOTIFY */]: {
44338
44414
  channel: (p) => p.userId ? SERVER_TO_FRONTENDS.notify.user(p.userId) : SERVER_TO_FRONTENDS.todo.notify(p.todoId),
44339
44415
  audience: "frontend"
@@ -44450,7 +44526,7 @@ import { parseArgs } from "util";
44450
44526
  // package.json
44451
44527
  var package_default = {
44452
44528
  name: "@todoforai/cli",
44453
- version: "0.1.47",
44529
+ version: "0.1.48",
44454
44530
  type: "module",
44455
44531
  bin: {
44456
44532
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44507,7 +44583,11 @@ Usage:
44507
44583
  tfa-cli acp # Agent Client Protocol server on stdio (Zed, JetBrains, …; see README)
44508
44584
  tfa-cli --list-agents # List available agents and exit
44509
44585
  tfa-cli --list-models [filter] # List models usable with --model and exit
44510
- tfa-cli agent update <agent> model=<model> # Update agent settings (see 'agent --help')
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')
44511
44591
  tfa-cli list [-n 30] [--cursor N] [--all] [--status S] # List todos (paginated); see 'list --help'
44512
44592
  tfa-cli status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
44513
44593
  tfa-cli delete <todo-id> # Permanently delete a todo
@@ -44614,6 +44694,10 @@ function parseCliArgs() {
44614
44694
  link: { type: "boolean", default: false },
44615
44695
  direction: { type: "string" },
44616
44696
  "business-context": { type: "string" },
44697
+ brand: { type: "string" },
44698
+ url: { type: "string" },
44699
+ "pasted-file": { type: "string" },
44700
+ "from-company": { type: "boolean", default: false },
44617
44701
  seed: { type: "string" },
44618
44702
  emails: { type: "string" },
44619
44703
  ttl: { type: "string" },
@@ -47290,6 +47374,9 @@ Usage:
47290
47374
  tfa-cli agent list List agents (name, model, id, paths)
47291
47375
  tfa-cli agent get <agent> Show a single agent's settings
47292
47376
  tfa-cli agent update <agent> <field=value>… Update one or more settings
47377
+ tfa-cli agent create [<field=value>…] Create an agent (defaults, then apply fields)
47378
+ tfa-cli agent global [<field=value>…] Show / update the user's global agent defaults
47379
+ tfa-cli agent reorder <agent>… Set the sidebar order (names or ids)
47293
47380
  tfa-cli agent delete <agent> Delete an agent configuration (asks to confirm)
47294
47381
 
47295
47382
  <agent> is a name or id (unique partial name also works).
@@ -47306,6 +47393,10 @@ Examples:
47306
47393
  tfa-cli agent update <agent> model=claude
47307
47394
  tfa-cli agent update <agent> model=anthropic:anthropic/claude-opus-5 temperature=0.5
47308
47395
  tfa-cli agent update <agent> sysmsg="You are a terse video editor."
47396
+ tfa-cli agent create name=Reviewer model=claude sysmsg="Review diffs only."
47397
+
47398
+ From an agent shell (dst_ token) permissions, mcpConfigs, edgesMcpConfigs,
47399
+ devicesConfig and delete are refused by the backend.
47309
47400
  `);
47310
47401
  }
47311
47402
  var FIELD_ALIASES = {
@@ -47314,7 +47405,7 @@ var FIELD_ALIASES = {
47314
47405
  temp: "temperature",
47315
47406
  thinking: "thinkingLevel"
47316
47407
  };
47317
- function parseAssignment(arg) {
47408
+ function parseAssignment(arg, aliases = {}) {
47318
47409
  const eq = arg.indexOf("=");
47319
47410
  if (eq < 1) {
47320
47411
  process.stderr.write(`${RED}Invalid field assignment '${arg}', expected field=value${RESET}
@@ -47322,7 +47413,7 @@ function parseAssignment(arg) {
47322
47413
  process.exit(2);
47323
47414
  }
47324
47415
  const rawKey = arg.slice(0, eq);
47325
- const key = FIELD_ALIASES[rawKey] || rawKey;
47416
+ const key = aliases[rawKey] || rawKey;
47326
47417
  const raw = arg.slice(eq + 1);
47327
47418
  try {
47328
47419
  return [key, JSON.parse(raw)];
@@ -47330,6 +47421,9 @@ function parseAssignment(arg) {
47330
47421
  return [key, raw];
47331
47422
  }
47332
47423
  }
47424
+ function parseAssignments(args, aliases = {}) {
47425
+ return Object.fromEntries(args.map((a) => parseAssignment(a, aliases)));
47426
+ }
47333
47427
  function resolveAgent(agents, query) {
47334
47428
  const { match, ambiguous } = resolveAgentMatch2(agents, query);
47335
47429
  if (match)
@@ -47377,7 +47471,7 @@ async function agentCommand(api, positionals, args, formatPath) {
47377
47471
  `);
47378
47472
  process.exit(2);
47379
47473
  }
47380
- const updates = Object.fromEntries(assignments.map(parseAssignment));
47474
+ const updates = parseAssignments(assignments, FIELD_ALIASES);
47381
47475
  const agents = await api.listAgentSettings();
47382
47476
  const agent = resolveAgent(agents, query);
47383
47477
  const id = getItemId2(agent);
@@ -47388,6 +47482,45 @@ async function agentCommand(api, positionals, args, formatPath) {
47388
47482
  }
47389
47483
  const summary = Object.keys(updates).map((k) => `${k}=${JSON.stringify(updated[k])}`).join(" ");
47390
47484
  process.stderr.write(`${GREEN}✅ ${getDisplayName2(agent)} updated: ${summary}${RESET}
47485
+ `);
47486
+ return;
47487
+ }
47488
+ if (sub === "global") {
47489
+ const updates = parseAssignments(positionals.slice(2), FIELD_ALIASES);
47490
+ const global = Object.keys(updates).length ? await api.updateGlobalAgentSettings(updates) : await api.getGlobalAgentSettings();
47491
+ if (args.json) {
47492
+ console.log(JSON.stringify(global, null, 2));
47493
+ return;
47494
+ }
47495
+ for (const [k, v] of Object.entries(global))
47496
+ process.stderr.write(` ${DIM}${k}:${RESET} ${typeof v === "string" ? v : JSON.stringify(v)}
47497
+ `);
47498
+ return;
47499
+ }
47500
+ if (sub === "reorder") {
47501
+ const queries = positionals.slice(2);
47502
+ if (!queries.length) {
47503
+ process.stderr.write(`${RED}Usage: tfa-cli agent reorder <agent>…${RESET}
47504
+ `);
47505
+ process.exit(2);
47506
+ }
47507
+ const agents = await api.listAgentSettings();
47508
+ const ids = queries.map((q) => getItemId2(resolveAgent(agents, q)));
47509
+ await api.reorderAgentSettings(ids);
47510
+ process.stderr.write(`${GREEN}✅ Agents reordered${RESET}
47511
+ `);
47512
+ return;
47513
+ }
47514
+ if (sub === "create") {
47515
+ const updates = parseAssignments(positionals.slice(2), FIELD_ALIASES);
47516
+ const created = await api.createAgent();
47517
+ const id = getItemId2(created);
47518
+ const agent = Object.keys(updates).length ? await api.updateAgentSettings(id, id, updates) : created;
47519
+ if (args.json) {
47520
+ console.log(JSON.stringify(agent, null, 2));
47521
+ return;
47522
+ }
47523
+ process.stderr.write(`${GREEN}✅ Created agent ${getDisplayName2(agent)}${RESET} ${DIM}${id}${RESET}
47391
47524
  `);
47392
47525
  return;
47393
47526
  }
@@ -47423,6 +47556,493 @@ async function agentCommand(api, positionals, args, formatPath) {
47423
47556
  process.exit(2);
47424
47557
  }
47425
47558
 
47559
+ // src/agent-command.ts
47560
+ function parseAssignment2(arg, aliases = {}) {
47561
+ const eq = arg.indexOf("=");
47562
+ if (eq < 1) {
47563
+ process.stderr.write(`${RED}Invalid field assignment '${arg}', expected field=value${RESET}
47564
+ `);
47565
+ process.exit(2);
47566
+ }
47567
+ const rawKey = arg.slice(0, eq);
47568
+ const key = aliases[rawKey] || rawKey;
47569
+ const raw = arg.slice(eq + 1);
47570
+ try {
47571
+ return [key, JSON.parse(raw)];
47572
+ } catch {
47573
+ return [key, raw];
47574
+ }
47575
+ }
47576
+ function parseAssignments2(args, aliases = {}) {
47577
+ return Object.fromEntries(args.map((a) => parseAssignment2(a, aliases)));
47578
+ }
47579
+ function resolveAgent2(agents, query) {
47580
+ const { match, ambiguous } = resolveAgentMatch2(agents, query);
47581
+ if (match)
47582
+ return match;
47583
+ if (ambiguous) {
47584
+ process.stderr.write(`${RED}Ambiguous agent '${query}' — ${ambiguous.length} matches. Re-run with the exact id:${RESET}
47585
+ `);
47586
+ for (const a of ambiguous)
47587
+ process.stderr.write(` ${getDisplayName2(a)} ${DIM}${getItemId2(a)}${RESET}
47588
+ `);
47589
+ process.exit(2);
47590
+ }
47591
+ process.stderr.write(`${RED}No agent matching '${query}'${RESET}
47592
+ `);
47593
+ process.exit(2);
47594
+ }
47595
+
47596
+ // src/args.ts
47597
+ var VERSION2 = package_default.version;
47598
+ function getEnv2(name) {
47599
+ return (process.env[`TODOFORAI_${name}`] || process.env[`TODO4AI_${name}`] || "").trim();
47600
+ }
47601
+ var STATUS_HELP2 = {
47602
+ ["READY" /* READY */]: "AI finished the work on the TODO",
47603
+ ["READY_CHECKED" /* READY_CHECKED */]: "AI finished and the user reviewed it",
47604
+ ["DONE" /* DONE */]: "Completed and finalized",
47605
+ ["REVIEW_REQUESTED" /* REVIEW_REQUESTED */]: "Asks the user to review",
47606
+ ["POSTPONED" /* POSTPONED */]: "Put off for later",
47607
+ ["ARCHIVED" /* ARCHIVED */]: "Archived (hidden from active list)",
47608
+ ["DELETED" /* DELETED */]: "Marked for deletion"
47609
+ };
47610
+
47611
+ // src/manage-command.ts
47612
+ function printTodoHelp() {
47613
+ process.stderr.write(`
47614
+ tfa-cli todo — edit a todo's fields
47615
+
47616
+ Usage:
47617
+ tfa-cli todo set <todo-id|-> <field=value>… Update fields ("-" = $TODOFORAI_TODO_ID)
47618
+ tfa-cli todo restore <todo-id> Bring a trashed todo back (undo of 'delete')
47619
+
47620
+ Fields (PUT /todos/{id}):
47621
+ title first-line content (alias: content)
47622
+ group group slug; "" makes it ungrouped (alias: groupTag)
47623
+ star true|false (starredAt now / 0)
47624
+ schedule unix ms timestamp, 0 = immediate (alias: scheduledTimestamp)
47625
+ agent agentSettingsId (alias: agentSettingsId)
47626
+ brand businessContextId (alias: businessContextId)
47627
+ public true|false — refused from an agent shell (isPublic)
47628
+
47629
+ tfa-cli todo message <todo-id|-> <message-id> <field=value>…
47630
+ Edit a SCHEDULED (not yet run) message: content, schedule (unix ms;
47631
+ 0 = run now). Runs with the todo's own agent.
47632
+
47633
+ Examples:
47634
+ tfa-cli todo set - title="Ship v2 landing" group=marketing
47635
+ tfa-cli todo set 3f2a91 star=true
47636
+ tfa-cli todo message - 8c1d… schedule=1760000000000
47637
+ tfa-cli status <todo-id> DONE (status has its own command)
47638
+ `);
47639
+ }
47640
+ function printProjectHelp() {
47641
+ process.stderr.write(`
47642
+ tfa-cli project — edit the current project
47643
+
47644
+ --project <id> or $TODOFORAI_PROJECT_ID selects the project.
47645
+
47646
+ Usage:
47647
+ tfa-cli project set <field=value>… name=… description=… brand=<businessContextId>
47648
+ (isPublic refused from an agent shell)
47649
+ tfa-cli project default Make it the project you land on at /
47650
+ tfa-cli project agent <agent> Default agent for new todos in this project
47651
+ tfa-cli project settings <field=value>… Project settings (PUT /settings/projects/{id})
47652
+ tfa-cli project groups List todo groups
47653
+ tfa-cli project groups set <slug> <field=value>… Create/update a group: name description pinned archived order
47654
+ tfa-cli project groups reorder <slug>… Set wall order
47655
+
47656
+ Examples:
47657
+ tfa-cli project set name="Q4 launch"
47658
+ tfa-cli project groups set marketing name=Marketing description="Top-of-funnel work"
47659
+ tfa-cli project groups reorder marketing sales ops
47660
+ `);
47661
+ }
47662
+ var TODO_ALIASES = {
47663
+ title: "content",
47664
+ group: "groupTag",
47665
+ schedule: "scheduledTimestamp",
47666
+ agent: "agentSettingsId",
47667
+ brand: "businessContextId",
47668
+ public: "isPublic"
47669
+ };
47670
+ var PROJECT_ALIASES = { brand: "businessContextId" };
47671
+ function fail(msg) {
47672
+ process.stderr.write(`${RED}${msg}${RESET}
47673
+ `);
47674
+ process.exit(2);
47675
+ }
47676
+ async function todoCommand(api, positionals, args) {
47677
+ const [, sub, idArg, ...rest] = positionals;
47678
+ if (sub === "restore") {
47679
+ if (!idArg)
47680
+ fail("Usage: tfa-cli todo restore <todo-id>");
47681
+ await api.restoreTodo(idArg);
47682
+ process.stderr.write(`${GREEN}✅ Restored ${idArg}${RESET}
47683
+ `);
47684
+ return;
47685
+ }
47686
+ const todoId = !idArg || idArg === "-" ? getEnv2("TODO_ID") : idArg;
47687
+ if (sub === "message") {
47688
+ const [messageId, ...fields] = rest;
47689
+ if (!todoId || !messageId || !fields.length)
47690
+ fail("Usage: tfa-cli todo message <todo-id|-> <message-id> <field=value>…");
47691
+ const updates2 = parseAssignments2(fields, { schedule: "scheduledTimestamp" });
47692
+ const agentSettingsId = (await api.getTodo(todoId))?.agentSettingsId;
47693
+ if (!agentSettingsId)
47694
+ fail("Todo has no agent assigned");
47695
+ await api.updateScheduledMessage(todoId, messageId, agentSettingsId, updates2);
47696
+ process.stderr.write(`${GREEN}✅ message ${messageId} updated: ${Object.keys(updates2).join(", ")}${RESET}
47697
+ `);
47698
+ return;
47699
+ }
47700
+ if (sub !== "set") {
47701
+ printTodoHelp();
47702
+ process.exit(sub ? 2 : 0);
47703
+ }
47704
+ if (!todoId || !rest.length)
47705
+ fail("Usage: tfa-cli todo set <todo-id|-> <field=value>…");
47706
+ const updates = parseAssignments2(rest, TODO_ALIASES);
47707
+ if ("star" in updates) {
47708
+ if (typeof updates.star !== "boolean")
47709
+ fail("star must be true or false");
47710
+ updates.starredAt = updates.star ? Date.now() : 0;
47711
+ delete updates.star;
47712
+ }
47713
+ await api.updateTodo(todoId, updates);
47714
+ if (args.json) {
47715
+ console.log(JSON.stringify({ todoId, ...updates }));
47716
+ return;
47717
+ }
47718
+ process.stderr.write(`${GREEN}✅ ${todoId} updated: ${Object.keys(updates).join(", ")}${RESET}
47719
+ `);
47720
+ }
47721
+ async function projectCommand(api, positionals, args) {
47722
+ const [, sub, ...rest] = positionals;
47723
+ const projectId = args.project || getEnv2("PROJECT_ID");
47724
+ if (!sub) {
47725
+ printProjectHelp();
47726
+ process.exit(0);
47727
+ }
47728
+ if (!projectId)
47729
+ fail("No project — pass --project <id> or set TODOFORAI_PROJECT_ID");
47730
+ if (sub === "set") {
47731
+ if (!rest.length)
47732
+ fail("Usage: tfa-cli project set <field=value>…");
47733
+ const updates = parseAssignments2(rest, PROJECT_ALIASES);
47734
+ await api.updateProject(projectId, updates);
47735
+ process.stderr.write(`${GREEN}✅ project updated: ${Object.keys(updates).join(", ")}${RESET}
47736
+ `);
47737
+ return;
47738
+ }
47739
+ if (sub === "default") {
47740
+ await api.setDefaultProject(projectId);
47741
+ process.stderr.write(`${GREEN}✅ ${projectId} is now the default project${RESET}
47742
+ `);
47743
+ return;
47744
+ }
47745
+ if (sub === "agent") {
47746
+ if (!rest[0])
47747
+ fail("Usage: tfa-cli project agent <agent>");
47748
+ const agent = resolveAgent2(await api.listAgentSettings(), rest[0]);
47749
+ await api.setProjectDefaultAgent(projectId, getItemId2(agent));
47750
+ process.stderr.write(`${GREEN}✅ default agent for ${projectId}: ${getDisplayName2(agent)}${RESET}
47751
+ `);
47752
+ return;
47753
+ }
47754
+ if (sub === "settings") {
47755
+ if (!rest.length)
47756
+ fail("Usage: tfa-cli project settings <field=value>…");
47757
+ const settings = parseAssignments2(rest);
47758
+ await api.updateProjectSettings(projectId, settings);
47759
+ process.stderr.write(`${GREEN}✅ project settings updated: ${Object.keys(settings).join(", ")}${RESET}
47760
+ `);
47761
+ return;
47762
+ }
47763
+ if (sub === "groups") {
47764
+ const [verb, ...gargs] = rest;
47765
+ if (!verb) {
47766
+ const groups = await api.listProjectGroups(projectId);
47767
+ if (args.json) {
47768
+ console.log(JSON.stringify(groups, null, 2));
47769
+ return;
47770
+ }
47771
+ for (const g of groups)
47772
+ process.stderr.write(`${getItemId2(g) || g.slug} ${g.name ?? ""} ${DIM}${g.description ?? ""}${RESET}
47773
+ `);
47774
+ return;
47775
+ }
47776
+ if (verb === "set") {
47777
+ const [slug, ...fields] = gargs;
47778
+ if (!slug)
47779
+ fail("Usage: tfa-cli project groups set <slug> <field=value>…");
47780
+ const group = await api.upsertProjectGroup(projectId, slug, parseAssignments2(fields));
47781
+ if (args.json) {
47782
+ console.log(JSON.stringify(group, null, 2));
47783
+ return;
47784
+ }
47785
+ process.stderr.write(`${GREEN}✅ group ${slug} saved${RESET}
47786
+ `);
47787
+ return;
47788
+ }
47789
+ if (verb === "reorder") {
47790
+ if (!gargs.length)
47791
+ fail("Usage: tfa-cli project groups reorder <slug>…");
47792
+ await api.reorderProjectGroups(projectId, gargs);
47793
+ process.stderr.write(`${GREEN}✅ groups reordered${RESET}
47794
+ `);
47795
+ return;
47796
+ }
47797
+ fail(`Unknown 'project groups' verb: ${verb}`);
47798
+ }
47799
+ fail(`Unknown 'project' subcommand: ${sub}`);
47800
+ }
47801
+ function printBrandHelp() {
47802
+ process.stderr.write(`
47803
+ tfa-cli brand — business contexts (brands) and the learned writing voice
47804
+
47805
+ The brand page text itself is a file: read/write todoforai:business-context.
47806
+ These commands manage the contexts and the voice learned for them.
47807
+
47808
+ Usage:
47809
+ tfa-cli brand list Contexts (* = selected)
47810
+ tfa-cli brand create <name> New brand (empty .md page)
47811
+ tfa-cli brand rename <brand> <name>
47812
+ tfa-cli brand select <brand|none> Active brand for the account
47813
+ tfa-cli brand voice Learned profile + sources
47814
+ 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
47819
+ tfa-cli brand voice remove <channel>
47820
+ tfa-cli brand voice learn [--from-company] Run the refinement loop, store the profile
47821
+
47822
+ <brand> is an id or name (unique partial works). Voice subcommands use the
47823
+ selected brand unless --brand <id|name> is given (--pasted-file - reads stdin).
47824
+ Delete / reset are UI-only.
47825
+
47826
+ Questions the UI asks (keys for 'answers'):
47827
+ "What makes you different from competitors?"
47828
+ "What are you working on right now?"
47829
+ "A frustrated customer says things aren't working. How do you reply?"
47830
+ `);
47831
+ }
47832
+ async function resolveBrand(api, query, selectedId) {
47833
+ const contexts = await api.listBusinessContexts();
47834
+ if (!query) {
47835
+ const sel = contexts.find((c3) => c3.id === selectedId);
47836
+ if (sel)
47837
+ return sel;
47838
+ if (contexts.length === 1)
47839
+ return contexts[0];
47840
+ fail("No brand selected — pass --brand <id|name> or 'tfa-cli brand select'");
47841
+ }
47842
+ const q = query.toLowerCase();
47843
+ const exact = contexts.find((c3) => c3.id === query || c3.name.toLowerCase() === q);
47844
+ if (exact)
47845
+ return exact;
47846
+ const partial = contexts.filter((c3) => c3.id.startsWith(query) || c3.name.toLowerCase().includes(q));
47847
+ if (partial.length === 1)
47848
+ return partial[0];
47849
+ fail(partial.length ? `Ambiguous brand '${query}'` : `No brand matching '${query}'`);
47850
+ }
47851
+ async function brandCommand(api, positionals, args) {
47852
+ const [, sub, ...rest] = positionals;
47853
+ if (!sub || sub === "list") {
47854
+ const contexts = await api.listBusinessContexts();
47855
+ const selected = (await api.getProfile()).user?.selectedBusinessContextId;
47856
+ if (args.json) {
47857
+ console.log(JSON.stringify(contexts.map((c3) => ({ ...c3, selected: c3.id === selected })), null, 2));
47858
+ return;
47859
+ }
47860
+ for (const c3 of contexts)
47861
+ process.stderr.write(`${c3.id === selected ? "*" : " "} ${c3.name} ${DIM}${c3.id}${RESET}
47862
+ `);
47863
+ return;
47864
+ }
47865
+ if (sub === "create") {
47866
+ if (!rest[0])
47867
+ fail("Usage: tfa-cli brand create <name>");
47868
+ const c3 = await api.createBusinessContext(rest[0]);
47869
+ if (args.json) {
47870
+ console.log(JSON.stringify(c3, null, 2));
47871
+ return;
47872
+ }
47873
+ process.stderr.write(`${GREEN}✅ brand ${c3.name} created ${DIM}${c3.id}${RESET}
47874
+ `);
47875
+ return;
47876
+ }
47877
+ if (sub === "rename") {
47878
+ if (!rest[1])
47879
+ fail("Usage: tfa-cli brand rename <brand> <name>");
47880
+ const c3 = await resolveBrand(api, rest[0]);
47881
+ await api.renameBusinessContext(c3.id, rest[1]);
47882
+ process.stderr.write(`${GREEN}✅ renamed to ${rest[1]}${RESET}
47883
+ `);
47884
+ return;
47885
+ }
47886
+ if (sub === "select") {
47887
+ if (!rest[0])
47888
+ fail("Usage: tfa-cli brand select <brand|none>");
47889
+ const id = rest[0] === "none" ? null : (await resolveBrand(api, rest[0])).id;
47890
+ await api.selectBusinessContext(id);
47891
+ process.stderr.write(`${GREEN}✅ selected brand: ${id ?? "none"}${RESET}
47892
+ `);
47893
+ return;
47894
+ }
47895
+ if (sub === "voice")
47896
+ return voiceCommand(api, rest, args);
47897
+ fail(`Unknown 'brand' subcommand: ${sub}`);
47898
+ }
47899
+ async function voiceCommand(api, rest, args) {
47900
+ const [verb, ...vargs] = rest;
47901
+ const onboarding = await api.getOnboarding();
47902
+ if (verb === "answers") {
47903
+ if (vargs.length) {
47904
+ const given = Object.fromEntries(vargs.map((a) => {
47905
+ const i = a.indexOf("=");
47906
+ if (i < 1)
47907
+ fail(`Expected question=answer, got '${a}'`);
47908
+ return [a.slice(0, i), a.slice(i + 1)];
47909
+ }));
47910
+ const styleAnswers = { ...onboarding.styleAnswers ?? {}, ...given };
47911
+ await api.patchOnboarding({ styleAnswers });
47912
+ process.stderr.write(`${GREEN}✅ ${vargs.length} answer(s) saved${RESET}
47913
+ `);
47914
+ return;
47915
+ }
47916
+ const answers = onboarding.styleAnswers ?? {};
47917
+ if (args.json) {
47918
+ console.log(JSON.stringify(answers, null, 2));
47919
+ return;
47920
+ }
47921
+ for (const [q, a] of Object.entries(answers))
47922
+ process.stderr.write(`${q}
47923
+ ${DIM}${a || "(empty)"}${RESET}
47924
+ `);
47925
+ return;
47926
+ }
47927
+ const brand = await resolveBrand(api, args.brand, (await api.getProfile()).user?.selectedBusinessContextId);
47928
+ if (!verb || verb === "show") {
47929
+ const profile = onboarding.voiceProfiles?.[brand.id];
47930
+ const { sources } = await api.listVoiceSources(brand.id);
47931
+ if (args.json) {
47932
+ console.log(JSON.stringify({ brand, profile, sources }, null, 2));
47933
+ return;
47934
+ }
47935
+ process.stderr.write(`${brand.name} ${DIM}${brand.id}${RESET}
47936
+ `);
47937
+ process.stderr.write(profile ? `
47938
+ ${profile.profile}
47939
+ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
47940
+ ` : `${DIM}(no voice learned yet)${RESET}
47941
+ `);
47942
+ for (const s of sources)
47943
+ process.stderr.write(` ${s.channel} ${DIM}${s.posts ?? "?"} posts · ${s.chars ?? "?"} chars${RESET}
47944
+ `);
47945
+ return;
47946
+ }
47947
+ if (verb === "collect") {
47948
+ const channel = vargs[0];
47949
+ if (!channel)
47950
+ fail("Usage: tfa-cli brand voice collect <channel> [--url U | --pasted-file F|-]");
47951
+ let pasted;
47952
+ if (args["pasted-file"]) {
47953
+ const { readFileSync: readFileSync3 } = await import("node:fs");
47954
+ pasted = readFileSync3(args["pasted-file"] === "-" ? 0 : args["pasted-file"], "utf8");
47955
+ }
47956
+ const { sources } = await api.collectVoiceSource(brand.id, channel, { url: args.url, pasted });
47957
+ process.stderr.write(`${GREEN}✅ ${channel} collected (${sources.length} source(s))${RESET}
47958
+ `);
47959
+ return;
47960
+ }
47961
+ if (verb === "remove") {
47962
+ if (!vargs[0])
47963
+ fail("Usage: tfa-cli brand voice remove <channel>");
47964
+ await api.removeVoiceSource(brand.id, vargs[0]);
47965
+ process.stderr.write(`${GREEN}✅ ${vargs[0]} removed${RESET}
47966
+ `);
47967
+ return;
47968
+ }
47969
+ if (verb === "learn") {
47970
+ process.stderr.write(`${DIM}learning voice for ${brand.name}…${RESET}
47971
+ `);
47972
+ const res = await api.refineBrandVoice(brand.id, onboarding.styleAnswers ?? {}, args["from-company"] ? "company" : undefined);
47973
+ if (!res.profile)
47974
+ fail("Nothing to learn from — add answers or collect a source first");
47975
+ const { [brand.id]: _stale, ...styleAiAnswers } = onboarding.styleAiAnswers ?? {};
47976
+ await api.patchOnboarding({
47977
+ voiceProfiles: { ...onboarding.voiceProfiles ?? {}, [brand.id]: { ...res, updatedAt: Date.now() } },
47978
+ styleAiAnswers
47979
+ });
47980
+ if (args.json) {
47981
+ console.log(JSON.stringify(res, null, 2));
47982
+ return;
47983
+ }
47984
+ process.stderr.write(`${GREEN}✅ voice learned (match ${res.match}/100, source ${res.source})${RESET}
47985
+ ${res.profile}
47986
+ `);
47987
+ return;
47988
+ }
47989
+ fail(`Unknown 'brand voice' verb: ${verb}`);
47990
+ }
47991
+ function printDeviceHelp() {
47992
+ process.stderr.write(`
47993
+ tfa-cli device — the user's paired machines
47994
+
47995
+ Usage:
47996
+ tfa-cli device list
47997
+ tfa-cli device rename <device> <name> name: [a-zA-Z0-9][a-zA-Z0-9_]*, ≤64
47998
+ tfa-cli device wallpaper <device> auto|off desktop-wallpaper capture (off deletes stored images)
47999
+
48000
+ <device> is an id, name or hostname (unique partial works).
48001
+ exec / reboot / update / unpair / workspace paths are UI-only from an agent shell.
48002
+ `);
48003
+ }
48004
+ async function deviceCommand(api, positionals, args) {
48005
+ const [, sub, query, value] = positionals;
48006
+ const devices = await api.listDevices();
48007
+ if (!sub || sub === "list") {
48008
+ if (args.json) {
48009
+ console.log(JSON.stringify(devices, null, 2));
48010
+ return;
48011
+ }
48012
+ for (const d of devices)
48013
+ process.stderr.write(`${d.status === "ONLINE" ? GREEN + "●" : DIM + "○"}${RESET} ${d.name} ${DIM}${d.deviceType} ${d.metadata?.identity?.hostname ?? ""} ${d.id}${RESET}
48014
+ `);
48015
+ return;
48016
+ }
48017
+ if (!query)
48018
+ fail(`Usage: tfa-cli device ${sub} <device> …`);
48019
+ const q = query.toLowerCase();
48020
+ const hit = devices.filter((d) => d.id === query || d.name.toLowerCase() === q || d.metadata?.identity?.hostname?.toLowerCase() === q);
48021
+ const found = hit.length === 1 ? hit[0] : (() => {
48022
+ const p = devices.filter((d) => d.id.startsWith(query) || d.name.toLowerCase().includes(q) || d.metadata?.identity?.hostname?.toLowerCase().includes(q));
48023
+ if (p.length === 1)
48024
+ return p[0];
48025
+ fail(p.length ? `Ambiguous device '${query}'` : `No device matching '${query}'`);
48026
+ })();
48027
+ if (sub === "rename") {
48028
+ if (!value)
48029
+ fail("Usage: tfa-cli device rename <device> <name>");
48030
+ await api.renameDevice(found.id, value);
48031
+ process.stderr.write(`${GREEN}✅ ${found.name} → ${value}${RESET}
48032
+ `);
48033
+ return;
48034
+ }
48035
+ if (sub === "wallpaper") {
48036
+ if (value !== "auto" && value !== "off")
48037
+ fail("Usage: tfa-cli device wallpaper <device> auto|off");
48038
+ await api.setDeviceWallpaperMode(found.id, value);
48039
+ process.stderr.write(`${GREEN}✅ ${found.name} wallpaper: ${value}${RESET}
48040
+ `);
48041
+ return;
48042
+ }
48043
+ fail(`Unknown 'device' subcommand: ${sub}`);
48044
+ }
48045
+
47426
48046
  // src/list-todos.ts
47427
48047
  import { parseArgs as parseArgs2 } from "util";
47428
48048
  var STATUS_COLOR = {
@@ -72037,11 +72657,27 @@ Cancelled by user (Ctrl+C)
72037
72657
  printAgentHelp();
72038
72658
  process.exit(0);
72039
72659
  }
72660
+ if (positionals[0] === "todo" && args.help) {
72661
+ printTodoHelp();
72662
+ process.exit(0);
72663
+ }
72664
+ if (positionals[0] === "project" && args.help) {
72665
+ printProjectHelp();
72666
+ process.exit(0);
72667
+ }
72668
+ if (positionals[0] === "brand" && args.help) {
72669
+ printBrandHelp();
72670
+ process.exit(0);
72671
+ }
72672
+ if (positionals[0] === "device" && args.help) {
72673
+ printDeviceHelp();
72674
+ process.exit(0);
72675
+ }
72040
72676
  if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
72041
72677
  printListTodosHelp();
72042
72678
  process.exit(0);
72043
72679
  }
72044
- if (args.help && !["list", "ls", "agent"].includes(positionals[0])) {
72680
+ if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device"].includes(positionals[0])) {
72045
72681
  printUsage();
72046
72682
  process.exit(0);
72047
72683
  }
@@ -72153,6 +72789,22 @@ Cancelled by user (Ctrl+C)
72153
72789
  `);
72154
72790
  return;
72155
72791
  }
72792
+ if (positionals[0] === "todo") {
72793
+ await todoCommand(api2, positionals, args);
72794
+ return;
72795
+ }
72796
+ if (positionals[0] === "project") {
72797
+ await projectCommand(api2, positionals, args);
72798
+ return;
72799
+ }
72800
+ if (positionals[0] === "brand") {
72801
+ await brandCommand(api2, positionals, args);
72802
+ return;
72803
+ }
72804
+ if (positionals[0] === "device") {
72805
+ await deviceCommand(api2, positionals, args);
72806
+ return;
72807
+ }
72156
72808
  if (positionals[0] === "delete") {
72157
72809
  const todoId = positionals[1];
72158
72810
  if (!todoId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "bin/todoforai-cli.js",