@todoforai/cli 0.1.49 → 0.1.51

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 +396 -31
  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");
@@ -42926,6 +42926,15 @@ class ApiClient {
42926
42926
  upsertProjectGroup(projectId, slug, fields) {
42927
42927
  return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
42928
42928
  }
42929
+ listProjectSurfaces(projectId) {
42930
+ return this.request("GET", `/api/v1/projects/${projectId}/surfaces`);
42931
+ }
42932
+ upsertProjectSurface(projectId, surfaceId, fields) {
42933
+ return this.request("PUT", `/api/v1/projects/${projectId}/surfaces/${encodeURIComponent(surfaceId)}`, { projectId, surfaceId, ...fields });
42934
+ }
42935
+ deleteProjectSurface(projectId, surfaceId) {
42936
+ return this.request("DELETE", `/api/v1/projects/${projectId}/surfaces/${encodeURIComponent(surfaceId)}`, { projectId, surfaceId });
42937
+ }
42929
42938
  reorderProjectGroups(projectId, slugs) {
42930
42939
  return this.request("PUT", `/api/v1/projects/${projectId}/groups-order`, { projectId, slugs });
42931
42940
  }
@@ -43729,6 +43738,12 @@ var SERVER_TO_FRONTENDS = {
43729
43738
  hexGrid: {
43730
43739
  updated: (projectId) => `${"hexgrid:updated" /* HEXGRID_UPDATED */}:${projectId}`
43731
43740
  },
43741
+ surfaces: {
43742
+ updated: (projectId) => `${"project:surfaces_updated" /* PROJECT_SURFACES_UPDATED */}:${projectId}`
43743
+ },
43744
+ killSwitch: {
43745
+ changed: (projectId) => `${"project:kill_switch" /* KILL_SWITCH */}:${projectId}`
43746
+ },
43732
43747
  sandbox: {
43733
43748
  state: (userId) => `${"sandbox:state" /* SANDBOX_STATE */}:${userId}`
43734
43749
  },
@@ -44390,6 +44405,14 @@ var TOPICS = {
44390
44405
  channel: (p) => SERVER_TO_FRONTENDS.hexGrid.updated(p.projectId),
44391
44406
  audience: "frontend"
44392
44407
  },
44408
+ ["project:surfaces_updated" /* PROJECT_SURFACES_UPDATED */]: {
44409
+ channel: (p) => SERVER_TO_FRONTENDS.surfaces.updated(p.projectId),
44410
+ audience: "frontend"
44411
+ },
44412
+ ["project:kill_switch" /* KILL_SWITCH */]: {
44413
+ channel: (p) => SERVER_TO_FRONTENDS.killSwitch.changed(p.projectId),
44414
+ audience: "frontend"
44415
+ },
44393
44416
  ["sandbox:state" /* SANDBOX_STATE */]: {
44394
44417
  channel: (p) => SERVER_TO_FRONTENDS.sandbox.state(p.userId),
44395
44418
  audience: "frontend"
@@ -44526,7 +44549,7 @@ import { parseArgs } from "util";
44526
44549
  // package.json
44527
44550
  var package_default = {
44528
44551
  name: "@todoforai/cli",
44529
- version: "0.1.49",
44552
+ version: "0.1.51",
44530
44553
  type: "module",
44531
44554
  bin: {
44532
44555
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44706,6 +44729,8 @@ function parseCliArgs() {
44706
44729
  url: { type: "string" },
44707
44730
  "pasted-file": { type: "string" },
44708
44731
  "from-company": { type: "boolean", default: false },
44732
+ "dry-run": { type: "boolean", default: false },
44733
+ max: { type: "string" },
44709
44734
  seed: { type: "string" },
44710
44735
  emails: { type: "string" },
44711
44736
  ttl: { type: "string" },
@@ -47625,6 +47650,180 @@ var STATUS_HELP2 = {
47625
47650
  ["DELETED" /* DELETED */]: "Marked for deletion"
47626
47651
  };
47627
47652
 
47653
+ // src/voice-collect.ts
47654
+ import { execFileSync } from "node:child_process";
47655
+ var run = (bin, args) => execFileSync(bin, args, { encoding: "utf8", maxBuffer: 256 << 20, timeout: 60000, stdio: ["ignore", "pipe", "ignore"] });
47656
+ var installed = (bin) => {
47657
+ try {
47658
+ run("which", [bin]);
47659
+ return true;
47660
+ } catch {
47661
+ return false;
47662
+ }
47663
+ };
47664
+ var MIN_CHARS = 30;
47665
+ var SEP = /^─{20,}$/m;
47666
+ function parseThread(out) {
47667
+ return out.split(SEP).slice(1).map((chunk) => {
47668
+ const lines = chunk.trim().split(`
47669
+ `);
47670
+ let i = 0;
47671
+ const h = {};
47672
+ for (;i < lines.length && /^\s*(From|To|Date|Auth|Cc):/.test(lines[i]); i++) {
47673
+ const [k, ...v] = lines[i].trim().split(":");
47674
+ h[k] = v.join(":").trim();
47675
+ }
47676
+ return { from: h.From ?? "", date: h.Date ?? "", body: lines.slice(i).join(`
47677
+ `).trim() };
47678
+ }).filter((m) => m.from);
47679
+ }
47680
+ function stripReply(body) {
47681
+ const lines = body.split(`
47682
+ `);
47683
+ 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()));
47684
+ let kept = (cut >= 0 ? lines.slice(0, cut) : lines).filter((l) => !/^Attachments: /.test(l)).join(`
47685
+ `).trim();
47686
+ kept = kept.replace(/\n--\s*\n[\s\S]*$/, "").trim();
47687
+ return kept;
47688
+ }
47689
+ var gmail = {
47690
+ tool: "zele",
47691
+ check: () => {
47692
+ if (!installed("zele"))
47693
+ return "zele is not installed";
47694
+ const who = (() => {
47695
+ try {
47696
+ return run("zele", ["whoami"]);
47697
+ } catch {
47698
+ return "";
47699
+ }
47700
+ })();
47701
+ return /email:/.test(who) ? null : "zele is not signed in (run: zele login --method google)";
47702
+ },
47703
+ collect: (max) => {
47704
+ const accounts = [...run("zele", ["whoami"]).matchAll(/email: (\S+)/g)].map((m) => m[1]);
47705
+ const isMe = (from) => accounts.some((a) => from.toLowerCase().includes(a.toLowerCase()));
47706
+ const samples = [];
47707
+ for (const account of accounts) {
47708
+ const list = run("zele", ["mail", "search", "from:me", "--limit", String(max), "--account", account]);
47709
+ const ids = [...list.matchAll(/^ - id: ([0-9a-f]+)\n(?:.*\n)*? messages: (\d+)/gm)].filter((m) => Number(m[2]) > 1).map((m) => m[1]);
47710
+ for (const id of ids) {
47711
+ if (samples.length >= max)
47712
+ break;
47713
+ const msgs = parseThread(run("zele", ["mail", "read", id, "--account", account]));
47714
+ for (let i = 1;i < msgs.length && samples.length < max; i++) {
47715
+ if (!isMe(msgs[i].from) || isMe(msgs[i - 1].from))
47716
+ continue;
47717
+ const text = stripReply(msgs[i].body), prompt = stripReply(msgs[i - 1].body);
47718
+ if (text.length < MIN_CHARS || prompt.length < 20)
47719
+ continue;
47720
+ samples.push({ at: msgs[i].date, text, prompt, url: `gmail:${account}/${id}#${i}` });
47721
+ }
47722
+ }
47723
+ }
47724
+ return { samples, note: `${accounts.length} account(s)` };
47725
+ }
47726
+ };
47727
+ var outlook = {
47728
+ tool: "outlook-api",
47729
+ check: () => {
47730
+ if (!installed("outlook-api"))
47731
+ return "outlook-api is not installed";
47732
+ try {
47733
+ const me = JSON.parse(run("outlook-api", ["whoami"]));
47734
+ return me?.mail || me?.userPrincipalName ? null : "outlook-api is not signed in";
47735
+ } catch {
47736
+ return "outlook-api is not signed in (run: outlook-api auth)";
47737
+ }
47738
+ },
47739
+ collect: (max) => {
47740
+ const me = JSON.parse(run("outlook-api", ["whoami"]));
47741
+ const mine = [me.mail, me.userPrincipalName].filter(Boolean).map((a) => a.toLowerCase());
47742
+ const isMe = (m) => mine.includes((m.from?.emailAddress?.address ?? "").toLowerCase());
47743
+ const sent = JSON.parse(run("outlook-api", ["list", "-f", "sentitems", "-n", String(max), "--select", "id,conversationId,from,sentDateTime,webLink"]));
47744
+ const sentRows = Array.isArray(sent) ? sent : sent.value ?? [];
47745
+ const samples = [];
47746
+ const seenConv = new Set;
47747
+ for (const s of sentRows) {
47748
+ if (!s.conversationId || seenConv.has(s.conversationId))
47749
+ continue;
47750
+ seenConv.add(s.conversationId);
47751
+ 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`]));
47752
+ const msgs = (conv.value ?? []).sort((a, b) => (a.receivedDateTime ?? "").localeCompare(b.receivedDateTime ?? ""));
47753
+ const body = (id) => stripReply(JSON.parse(run("outlook-api", ["read", id, "--text"])).body?.content ?? "");
47754
+ for (let i = 1;i < msgs.length && samples.length < max; i++) {
47755
+ if (!isMe(msgs[i]) || isMe(msgs[i - 1]))
47756
+ continue;
47757
+ const text = body(msgs[i].id), prompt = body(msgs[i - 1].id);
47758
+ if (text.length < MIN_CHARS || prompt.length < 20)
47759
+ continue;
47760
+ samples.push({ at: msgs[i].sentDateTime ?? msgs[i].receivedDateTime, text, prompt, url: msgs[i].webLink ?? `outlook:${msgs[i].id}` });
47761
+ }
47762
+ if (samples.length >= max)
47763
+ break;
47764
+ }
47765
+ return { samples };
47766
+ }
47767
+ };
47768
+ 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(" ");
47769
+ var chat = {
47770
+ tool: "tfa-memory",
47771
+ check: () => installed("tfa-memory") ? null : "tfa-memory is not installed",
47772
+ collect: (max) => {
47773
+ const mem = (...a) => run("tfa-memory", a);
47774
+ const anchors = new Map;
47775
+ for (const q of QUERIES) {
47776
+ for (const line of mem("search", q, "--source", "todo", "--mode", "lexical", "--limit", "50").split(`
47777
+ `)) {
47778
+ const m = line.match(/todo\/([0-9a-f-]{36}):([0-9a-f-]{36})/);
47779
+ if (m && !anchors.has(m[1]))
47780
+ anchors.set(m[1], `todo/${m[1]}:${m[2]}`);
47781
+ }
47782
+ if (anchors.size >= max)
47783
+ break;
47784
+ }
47785
+ const samples = [];
47786
+ for (const anchor of [...anchors.values()].slice(0, max)) {
47787
+ let msgs;
47788
+ try {
47789
+ msgs = JSON.parse(mem("around", anchor, "--before", "200", "--after", "200", "--json"));
47790
+ } catch {
47791
+ continue;
47792
+ }
47793
+ msgs.sort((a, b) => a.createdAt - b.createdAt);
47794
+ for (let i = 1;i < msgs.length; i++) {
47795
+ if (msgs[i].meta?.role !== "user" || msgs[i - 1].meta?.role === "user")
47796
+ continue;
47797
+ const text = msgs[i].content.trim(), prompt = msgs[i - 1].content.trim();
47798
+ if (text.length < MIN_CHARS || prompt.length < 40)
47799
+ continue;
47800
+ if (/^\s*(#|\/|@|\{)/.test(text))
47801
+ continue;
47802
+ if (text.split(`
47803
+ `).length > 6)
47804
+ continue;
47805
+ if (/^Wait w_|\nWait w_[0-9a-f]+ '|^Connected \w+ \(card\)|^\[/m.test(text))
47806
+ continue;
47807
+ samples.push({ at: new Date(msgs[i].createdAt).toISOString(), text, prompt: prompt.slice(0, 4000), url: msgs[i].address });
47808
+ }
47809
+ }
47810
+ return { samples: samples.slice(0, max), note: `${anchors.size} todos` };
47811
+ }
47812
+ };
47813
+ var ADAPTERS = { Gmail: gmail, Outlook: outlook, Chat: chat };
47814
+ var DEVICE_CHANNELS = Object.keys(ADAPTERS);
47815
+ function checkChannel(channel) {
47816
+ const a = ADAPTERS[channel];
47817
+ return a ? a.check() : `${channel} is not read on the device (server-side: pass --url, or paste)`;
47818
+ }
47819
+ function collectChannel(channel, max = 40) {
47820
+ const a = ADAPTERS[channel];
47821
+ if (!a)
47822
+ throw new Error(`No device adapter for ${channel}`);
47823
+ const r = a.collect(Math.min(200, Math.max(1, Math.floor(Number(max) || 40))));
47824
+ return { ...r, samples: r.samples.filter((x) => x.text.trim().length >= MIN_CHARS) };
47825
+ }
47826
+
47628
47827
  // src/manage-command.ts
47629
47828
  function printTodoHelp() {
47630
47829
  process.stderr.write(`
@@ -47670,11 +47869,18 @@ Usage:
47670
47869
  tfa-cli project groups List todo groups
47671
47870
  tfa-cli project groups set <slug> <field=value>… Create/update a group: name description pinned archived order
47672
47871
  tfa-cli project groups reorder <slug>… Set wall order
47872
+ tfa-cli project surfaces List the board's card surfaces (live refs shown as cards)
47873
+ tfa-cli project surfaces set <id> ref=<ref> [title=…] [group=<slug>] [order=N]
47874
+ Show a ref on the board: a todo id, <todoId>:<alias>
47875
+ (shown artifact) or an http(s) url. group= puts it
47876
+ inside that group's card; group="" makes it standalone.
47877
+ tfa-cli project surfaces rm <id> Take it off the board
47673
47878
 
47674
47879
  Examples:
47675
47880
  tfa-cli project set name="Q4 launch"
47676
47881
  tfa-cli project groups set marketing name=Marketing description="Top-of-funnel work"
47677
47882
  tfa-cli project groups reorder marketing sales ops
47883
+ tfa-cli project surfaces set weekly-kpis ref=$TODOFORAI_TODO_ID:kpi title="Weekly KPIs" group=marketing
47678
47884
  `);
47679
47885
  }
47680
47886
  var TODO_ALIASES = {
@@ -47827,6 +48033,43 @@ async function projectCommand(api, positionals, args) {
47827
48033
  }
47828
48034
  fail(`Unknown 'project groups' verb: ${verb}`);
47829
48035
  }
48036
+ if (sub === "surfaces") {
48037
+ const [verb, ...sargs] = rest;
48038
+ if (!verb) {
48039
+ const surfaces = await api.listProjectSurfaces(projectId);
48040
+ if (args.json) {
48041
+ console.log(JSON.stringify(surfaces, null, 2));
48042
+ return;
48043
+ }
48044
+ for (const s of surfaces)
48045
+ process.stderr.write(`${s.id} ${s.ref} ${DIM}${[s.title, s.group && `in ${s.group}`].filter(Boolean).join(" ")}${RESET}
48046
+ `);
48047
+ return;
48048
+ }
48049
+ if (verb === "set") {
48050
+ const [id, ...fields] = sargs;
48051
+ if (!id)
48052
+ fail("Usage: tfa-cli project surfaces set <id> ref=<ref> [title=…] [group=<slug>] [order=N]");
48053
+ const surface = await api.upsertProjectSurface(projectId, id, parseAssignments2(fields));
48054
+ if (args.json) {
48055
+ console.log(JSON.stringify(surface, null, 2));
48056
+ return;
48057
+ }
48058
+ process.stderr.write(`${GREEN}✅ surface ${id} shows ${surface.ref}${surface.group ? ` in ${surface.group}` : ""}${RESET}
48059
+ `);
48060
+ return;
48061
+ }
48062
+ if (verb === "rm") {
48063
+ const [id] = sargs;
48064
+ if (!id)
48065
+ fail("Usage: tfa-cli project surfaces rm <id>");
48066
+ await api.deleteProjectSurface(projectId, id);
48067
+ process.stderr.write(`${GREEN}✅ surface ${id} removed${RESET}
48068
+ `);
48069
+ return;
48070
+ }
48071
+ fail(`Unknown 'project surfaces' verb: ${verb}`);
48072
+ }
47830
48073
  fail(`Unknown 'project' subcommand: ${sub}`);
47831
48074
  }
47832
48075
  function printBrandHelp() {
@@ -47843,12 +48086,18 @@ Usage:
47843
48086
  tfa-cli brand select <brand|none> Active brand for the account
47844
48087
  tfa-cli brand voice Learned profile + sources
47845
48088
  tfa-cli brand voice answers [<q>=<a>…] Show / set the brand-voice answers (strings)
47846
- tfa-cli brand voice collect <channel> [--url U | --pasted-file <F|->]
47847
- Add a writing sample source
47848
- channels: X LinkedIn Facebook Instagram
47849
- Gmail Outlook Slack Teams
48089
+ tfa-cli brand voice collect <channel> [--url U | --pasted-file <F|-> | --max N]
48090
+ Add a writing sample source.
48091
+ On this device (signed-in CLI, reply pairs):
48092
+ Gmail (zele) Outlook (outlook-api) Chat (tfa-memory)
48093
+ Server-side (--url or paste):
48094
+ X LinkedIn Facebook Instagram Slack Teams
48095
+ --dry-run print the samples as JSONL, store nothing
48096
+ tfa-cli brand voice check [<channel>|--all] Can this device read the channel? tool, login, sample count
47850
48097
  tfa-cli brand voice remove <channel>
47851
48098
  tfa-cli brand voice learn [--from-company] Run the refinement loop, store the profile
48099
+ tfa-cli brand voice correct "<what is off>" Tell the learner what it got wrong; profile is updated
48100
+ in one pass and the correction is kept for every re-learn
47852
48101
 
47853
48102
  <brand> is an id or name (unique partial works). Voice subcommands use the
47854
48103
  selected brand unless --brand <id|name> is given (--pasted-file - reads stdin).
@@ -47927,8 +48176,61 @@ async function brandCommand(api, positionals, args) {
47927
48176
  return voiceCommand(api, rest, args);
47928
48177
  fail(`Unknown 'brand' subcommand: ${sub}`);
47929
48178
  }
48179
+ function dryCollect(channel, max) {
48180
+ const reason = checkChannel(channel);
48181
+ if (reason)
48182
+ fail(`${channel}: ${reason}`);
48183
+ const { samples, note } = collectChannel(channel, max);
48184
+ for (const x of samples)
48185
+ console.log(JSON.stringify(x));
48186
+ process.stderr.write(`${DIM}${samples.length} samples${note ? ` · ${note}` : ""} (not stored)${RESET}
48187
+ `);
48188
+ }
48189
+ async function voiceDeviceCommand(rest, args) {
48190
+ const [verb, ...vargs] = rest;
48191
+ if (verb === "check") {
48192
+ const channels3 = args.all || !vargs[0] ? DEVICE_CHANNELS : [vargs[0]];
48193
+ let failed = 0;
48194
+ const report = {};
48195
+ for (const ch of channels3) {
48196
+ const reason = checkChannel(ch);
48197
+ if (reason) {
48198
+ failed++;
48199
+ report[ch] = { ok: false, reason };
48200
+ continue;
48201
+ }
48202
+ try {
48203
+ const r = collectChannel(ch, Number(args.max ?? 10));
48204
+ const bad = r.samples.filter((x) => !x.text.trim() || ADAPTERS[ch] && !x.prompt).length;
48205
+ const ok = r.samples.length > 0 && bad === 0;
48206
+ if (!ok)
48207
+ failed++;
48208
+ 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" } };
48209
+ } catch (e) {
48210
+ failed++;
48211
+ report[ch] = { ok: false, reason: e.message };
48212
+ }
48213
+ }
48214
+ if (args.json)
48215
+ console.log(JSON.stringify(report, null, 2));
48216
+ else
48217
+ for (const [ch, r] of Object.entries(report))
48218
+ process.stderr.write(`${r.ok ? GREEN + "✅" : RED + "❌"} ${ch}${RESET} ${DIM}${r.ok ? `${r.samples} samples${r.note ? ` · ${r.note}` : ""}` : r.reason}${RESET}
48219
+ `);
48220
+ process.exit(failed ? 1 : 0);
48221
+ }
48222
+ if (verb === "collect" && args["dry-run"]) {
48223
+ if (!vargs[0])
48224
+ fail("Usage: tfa-cli brand voice collect <channel> --dry-run [--max N]");
48225
+ dryCollect(vargs[0], Number(args.max ?? 40));
48226
+ return true;
48227
+ }
48228
+ return false;
48229
+ }
47930
48230
  async function voiceCommand(api, rest, args) {
47931
48231
  const [verb, ...vargs] = rest;
48232
+ if (verb === "collect" && !vargs[0])
48233
+ fail("Usage: tfa-cli brand voice collect <channel> [--url U | --pasted-file F|- | --max N] [--dry-run]");
47932
48234
  const onboarding = await api.getOnboarding();
47933
48235
  if (verb === "answers") {
47934
48236
  if (vargs.length) {
@@ -47977,13 +48279,26 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
47977
48279
  }
47978
48280
  if (verb === "collect") {
47979
48281
  const channel = vargs[0];
47980
- if (!channel)
47981
- fail("Usage: tfa-cli brand voice collect <channel> [--url U | --pasted-file F|-]");
47982
48282
  let pasted;
47983
48283
  if (args["pasted-file"]) {
47984
48284
  const { readFileSync: readFileSync3 } = await import("node:fs");
47985
48285
  pasted = readFileSync3(args["pasted-file"] === "-" ? 0 : args["pasted-file"], "utf8");
47986
48286
  }
48287
+ const onDevice = !pasted && !args.url && ADAPTERS[channel];
48288
+ if (onDevice) {
48289
+ const reason = checkChannel(channel);
48290
+ if (reason)
48291
+ fail(`${channel}: ${reason}`);
48292
+ process.stderr.write(`${DIM}reading ${channel} on this device…${RESET}
48293
+ `);
48294
+ const { samples, note } = collectChannel(channel, Number(args.max ?? 40));
48295
+ if (!samples.length)
48296
+ fail(`${channel}: nothing usable came back${note ? ` (${note})` : ""}`);
48297
+ const { sources: sources2 } = await api.collectVoiceSource(brand.id, channel, { samples });
48298
+ process.stderr.write(`${GREEN}✅ ${channel}: ${samples.length} reply pairs stored (${sources2.length} source(s))${RESET}
48299
+ `);
48300
+ return;
48301
+ }
47987
48302
  const { sources } = await api.collectVoiceSource(brand.id, channel, { url: args.url, pasted });
47988
48303
  process.stderr.write(`${GREEN}✅ ${channel} collected (${sources.length} source(s))${RESET}
47989
48304
  `);
@@ -48014,6 +48329,31 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
48014
48329
  }
48015
48330
  process.stderr.write(`${GREEN}✅ voice learned (match ${res.match}/100, source ${res.source})${RESET}
48016
48331
  ${res.profile}
48332
+ `);
48333
+ return;
48334
+ }
48335
+ if (verb === "correct") {
48336
+ const text = vargs.join(" ").trim();
48337
+ if (!text)
48338
+ fail('Usage: tfa-cli brand voice correct "<what is off>"');
48339
+ const stored = onboarding.voiceProfiles?.[brand.id];
48340
+ if (!stored?.profile)
48341
+ fail("No voice learned yet — 'tfa-cli brand voice learn' first");
48342
+ process.stderr.write(`${DIM}applying correction…${RESET}
48343
+ `);
48344
+ const res = await api.refineBrandVoice(brand.id, onboarding.styleAnswers ?? {}, undefined, text);
48345
+ if (!res.profile)
48346
+ fail("The correction pass returned nothing — try rewording it");
48347
+ const { [brand.id]: _stale, ...styleAiAnswers } = onboarding.styleAiAnswers ?? {};
48348
+ await api.patchOnboarding({ voiceProfiles: { ...onboarding.voiceProfiles ?? {}, [brand.id]: { ...res, updatedAt: Date.now() } }, styleAiAnswers });
48349
+ if (args.json) {
48350
+ console.log(JSON.stringify(res, null, 2));
48351
+ return;
48352
+ }
48353
+ const last = res.iterations[res.iterations.length - 1];
48354
+ process.stderr.write(`${GREEN}✅ voice corrected (match ${res.match}/100)${RESET}
48355
+ ${res.profile}
48356
+ ${DIM}sample: ${last?.sample ?? ""}${RESET}
48017
48357
  `);
48018
48358
  return;
48019
48359
  }
@@ -48286,10 +48626,10 @@ function probeBridge2(bin) {
48286
48626
  function hasBridge2() {
48287
48627
  if (probeBridge2(bridgeBin2))
48288
48628
  return true;
48289
- const installed = path3.join(INSTALL_PREFIX2, "todoforai-bridge");
48290
- if (!probeBridge2(installed))
48629
+ const installed2 = path3.join(INSTALL_PREFIX2, "todoforai-bridge");
48630
+ if (!probeBridge2(installed2))
48291
48631
  return false;
48292
- bridgeBin2 = installed;
48632
+ bridgeBin2 = installed2;
48293
48633
  return true;
48294
48634
  }
48295
48635
  function installBridge2() {
@@ -67757,7 +68097,7 @@ function visit(schema, fnOrHandlers) {
67757
68097
  return h ? h(node2, rewritten) : node2;
67758
68098
  };
67759
68099
  const cache = new Map;
67760
- function run(s) {
68100
+ function run2(s) {
67761
68101
  const cached2 = cache.get(s);
67762
68102
  if (cached2 === RESOLVING) {
67763
68103
  return new $ZodLazy({
@@ -67783,21 +68123,21 @@ function visit(schema, fnOrHandlers) {
67783
68123
  let changed = false;
67784
68124
  const newShape = {};
67785
68125
  for (const k of keys) {
67786
- const mapped = run(oldShape[k]);
68126
+ const mapped = run2(oldShape[k]);
67787
68127
  if (mapped !== oldShape[k])
67788
68128
  changed = true;
67789
68129
  newShape[k] = mapped;
67790
68130
  }
67791
68131
  let newCatchall = def.catchall;
67792
68132
  if (def.catchall) {
67793
- newCatchall = run(def.catchall);
68133
+ newCatchall = run2(def.catchall);
67794
68134
  if (newCatchall !== def.catchall)
67795
68135
  changed = true;
67796
68136
  }
67797
68137
  return changed ? clone(s, { ...def, shape: newShape, catchall: newCatchall }) : s;
67798
68138
  }
67799
68139
  case "array": {
67800
- const mapped = run(def.element);
68140
+ const mapped = run2(def.element);
67801
68141
  return mapped === def.element ? s : clone(s, { ...def, element: mapped });
67802
68142
  }
67803
68143
  case "tuple": {
@@ -67805,14 +68145,14 @@ function visit(schema, fnOrHandlers) {
67805
68145
  let changed = false;
67806
68146
  const newItems = [];
67807
68147
  for (const item of oldItems) {
67808
- const mapped = run(item);
68148
+ const mapped = run2(item);
67809
68149
  if (mapped !== item)
67810
68150
  changed = true;
67811
68151
  newItems.push(mapped);
67812
68152
  }
67813
68153
  let newRest = def.rest;
67814
68154
  if (def.rest) {
67815
- newRest = run(def.rest);
68155
+ newRest = run2(def.rest);
67816
68156
  if (newRest !== def.rest)
67817
68157
  changed = true;
67818
68158
  }
@@ -67820,12 +68160,12 @@ function visit(schema, fnOrHandlers) {
67820
68160
  }
67821
68161
  case "record":
67822
68162
  case "map": {
67823
- const newKey = run(def.keyType);
67824
- const newVal = run(def.valueType);
68163
+ const newKey = run2(def.keyType);
68164
+ const newVal = run2(def.valueType);
67825
68165
  return newKey === def.keyType && newVal === def.valueType ? s : clone(s, { ...def, keyType: newKey, valueType: newVal });
67826
68166
  }
67827
68167
  case "set": {
67828
- const newVal = run(def.valueType);
68168
+ const newVal = run2(def.valueType);
67829
68169
  return newVal === def.valueType ? s : clone(s, { ...def, valueType: newVal });
67830
68170
  }
67831
68171
  case "union": {
@@ -67833,7 +68173,7 @@ function visit(schema, fnOrHandlers) {
67833
68173
  let changed = false;
67834
68174
  const newOptions = [];
67835
68175
  for (const opt of oldOptions) {
67836
- const mapped = run(opt);
68176
+ const mapped = run2(opt);
67837
68177
  if (mapped !== opt)
67838
68178
  changed = true;
67839
68179
  newOptions.push(mapped);
@@ -67841,8 +68181,8 @@ function visit(schema, fnOrHandlers) {
67841
68181
  return changed ? clone(s, { ...def, options: newOptions }) : s;
67842
68182
  }
67843
68183
  case "intersection": {
67844
- const newLeft = run(def.left);
67845
- const newRight = run(def.right);
68184
+ const newLeft = run2(def.left);
68185
+ const newRight = run2(def.right);
67846
68186
  return newLeft === def.left && newRight === def.right ? s : clone(s, { ...def, left: newLeft, right: newRight });
67847
68187
  }
67848
68188
  case "optional":
@@ -67854,23 +68194,23 @@ function visit(schema, fnOrHandlers) {
67854
68194
  case "nonoptional":
67855
68195
  case "promise":
67856
68196
  case "success": {
67857
- const newInner = run(def.innerType);
68197
+ const newInner = run2(def.innerType);
67858
68198
  return newInner === def.innerType ? s : clone(s, { ...def, innerType: newInner });
67859
68199
  }
67860
68200
  case "pipe": {
67861
- const newIn = run(def.in);
67862
- const newOut = run(def.out);
68201
+ const newIn = run2(def.in);
68202
+ const newOut = run2(def.out);
67863
68203
  return newIn === def.in && newOut === def.out ? s : clone(s, { ...def, in: newIn, out: newOut });
67864
68204
  }
67865
68205
  case "function": {
67866
- const newInput = run(def.input);
67867
- const newOutput = run(def.output);
68206
+ const newInput = run2(def.input);
68207
+ const newOutput = run2(def.output);
67868
68208
  return newInput === def.input && newOutput === def.output ? s : clone(s, { ...def, input: newInput, output: newOutput });
67869
68209
  }
67870
68210
  case "lazy": {
67871
68211
  const original = def.getter;
67872
68212
  const { _cachedInner, ...rest } = def;
67873
- return clone(s, { ...rest, getter: () => run(original()) });
68213
+ return clone(s, { ...rest, getter: () => run2(original()) });
67874
68214
  }
67875
68215
  case "template_literal":
67876
68216
  case "string":
@@ -67898,7 +68238,7 @@ function visit(schema, fnOrHandlers) {
67898
68238
  }
67899
68239
  }
67900
68240
  }
67901
- return run(schema);
68241
+ return run2(schema);
67902
68242
  }
67903
68243
 
67904
68244
  // node_modules/zod/v4/classic/deep-partial.js
@@ -69485,6 +69825,22 @@ var zUsageUpdate = object({
69485
69825
  return;
69486
69826
  })
69487
69827
  });
69828
+ var zNoticeSeverity = union([
69829
+ literal("info"),
69830
+ literal("warning"),
69831
+ literal("error"),
69832
+ string2()
69833
+ ]);
69834
+ var zNotice = object({
69835
+ severity: zNoticeSeverity,
69836
+ title: string2().min(1),
69837
+ description: defaultOnError(string2().nullish(), () => {
69838
+ return;
69839
+ }),
69840
+ _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
69841
+ return;
69842
+ })
69843
+ });
69488
69844
  var zCompactionId = string2();
69489
69845
  var zCompactionStatus = union([
69490
69846
  literal("in_progress"),
@@ -69553,6 +69909,9 @@ var zSessionUpdate = union([
69553
69909
  zUsageUpdate.and(object({
69554
69910
  sessionUpdate: literal("usage_update")
69555
69911
  })),
69912
+ zNotice.and(object({
69913
+ sessionUpdate: literal("notice")
69914
+ })),
69556
69915
  zCompactionUpdate.and(object({
69557
69916
  sessionUpdate: literal("compaction_update")
69558
69917
  })),
@@ -69614,6 +69973,7 @@ var zSessionConfigOptionsCapabilities = object({
69614
69973
  return;
69615
69974
  })
69616
69975
  });
69976
+ var zNoticeCapabilities = record(string2(), unknown());
69617
69977
  var zClientSessionCapabilities = object({
69618
69978
  compaction: defaultOnError(zCompactionCapabilities.nullish(), () => {
69619
69979
  return;
@@ -69621,6 +69981,9 @@ var zClientSessionCapabilities = object({
69621
69981
  configOptions: defaultOnError(zSessionConfigOptionsCapabilities.nullish(), () => {
69622
69982
  return;
69623
69983
  }),
69984
+ notices: defaultOnError(zNoticeCapabilities.nullish(), () => {
69985
+ return;
69986
+ }),
69624
69987
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => {
69625
69988
  return;
69626
69989
  })
@@ -72769,6 +73132,8 @@ Cancelled by user (Ctrl+C)
72769
73132
  process.exit(2);
72770
73133
  }
72771
73134
  }
73135
+ if (positionals[0] === "brand" && positionals[1] === "voice" && await voiceDeviceCommand(positionals.slice(2), args))
73136
+ return;
72772
73137
  const deviceLogin = () => runDeviceLogin(apiUrl).catch((e) => {
72773
73138
  if (!(e instanceof DeviceLoginError))
72774
73139
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "bin/todoforai-cli.js",