@todoforai/cli 0.1.65 → 0.1.66

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 +158 -77
  2. package/package.json +1 -1
@@ -37682,19 +37682,19 @@ var require_doctype = __commonJS((exports) => {
37682
37682
  return DOCUMENT_MODE.NO_QUIRKS;
37683
37683
  };
37684
37684
  exports.serializeContent = function(name, publicId, systemId) {
37685
- let str = "!DOCTYPE ";
37685
+ let str2 = "!DOCTYPE ";
37686
37686
  if (name) {
37687
- str += name;
37687
+ str2 += name;
37688
37688
  }
37689
37689
  if (publicId) {
37690
- str += " PUBLIC " + enquoteDoctypeId(publicId);
37690
+ str2 += " PUBLIC " + enquoteDoctypeId(publicId);
37691
37691
  } else if (systemId) {
37692
- str += " SYSTEM";
37692
+ str2 += " SYSTEM";
37693
37693
  }
37694
37694
  if (systemId !== null) {
37695
- str += " " + enquoteDoctypeId(systemId);
37695
+ str2 += " " + enquoteDoctypeId(systemId);
37696
37696
  }
37697
- return str;
37697
+ return str2;
37698
37698
  };
37699
37699
  });
37700
37700
 
@@ -40266,14 +40266,14 @@ var require_serializer = __commonJS((exports, module) => {
40266
40266
  this.html += "<" + doctype.serializeContent(name, null, null) + ">";
40267
40267
  }
40268
40268
  }
40269
- Serializer.escapeString = function(str, attrMode) {
40270
- str = str.replace(AMP_REGEX, "&amp;").replace(NBSP_REGEX, "&nbsp;");
40269
+ Serializer.escapeString = function(str2, attrMode) {
40270
+ str2 = str2.replace(AMP_REGEX, "&amp;").replace(NBSP_REGEX, "&nbsp;");
40271
40271
  if (attrMode) {
40272
- str = str.replace(DOUBLE_QUOTE_REGEX, "&quot;");
40272
+ str2 = str2.replace(DOUBLE_QUOTE_REGEX, "&quot;");
40273
40273
  } else {
40274
- str = str.replace(LT_REGEX, "&lt;").replace(GT_REGEX, "&gt;");
40274
+ str2 = str2.replace(LT_REGEX, "&lt;").replace(GT_REGEX, "&gt;");
40275
40275
  }
40276
- return str;
40276
+ return str2;
40277
40277
  };
40278
40278
  module.exports = Serializer;
40279
40279
  });
@@ -40664,19 +40664,19 @@ var require_doctype2 = __commonJS((exports) => {
40664
40664
  return DOCUMENT_MODE.NO_QUIRKS;
40665
40665
  };
40666
40666
  exports.serializeContent = function(name, publicId, systemId) {
40667
- let str = "!DOCTYPE ";
40667
+ let str2 = "!DOCTYPE ";
40668
40668
  if (name) {
40669
- str += name;
40669
+ str2 += name;
40670
40670
  }
40671
40671
  if (publicId) {
40672
- str += " PUBLIC " + enquoteDoctypeId(publicId);
40672
+ str2 += " PUBLIC " + enquoteDoctypeId(publicId);
40673
40673
  } else if (systemId) {
40674
- str += " SYSTEM";
40674
+ str2 += " SYSTEM";
40675
40675
  }
40676
40676
  if (systemId !== null) {
40677
- str += " " + enquoteDoctypeId(systemId);
40677
+ str2 += " " + enquoteDoctypeId(systemId);
40678
40678
  }
40679
- return str;
40679
+ return str2;
40680
40680
  };
40681
40681
  });
40682
40682
 
@@ -42981,8 +42981,8 @@ class ApiClient {
42981
42981
  renameBusinessContext(projectId, name) {
42982
42982
  return this.request("PUT", `/api/v1/projects/${projectId}/business-context`, { projectId, name });
42983
42983
  }
42984
- listVoiceSources(projectId) {
42985
- return this.request("GET", `/api/v1/projects/${projectId}/voice-sources`);
42984
+ listVoiceSources(projectId, scope) {
42985
+ return this.request("GET", `/api/v1/projects/${projectId}/voice-sources${scope ? `?scope=${scope}` : ""}`);
42986
42986
  }
42987
42987
  collectVoiceSource(projectId, channel, opts) {
42988
42988
  return this.request("POST", "/api/v1/business-context/collect-voice-source", { projectId, channel, ...opts }, 130000);
@@ -42990,8 +42990,8 @@ class ApiClient {
42990
42990
  removeVoiceSource(projectId, channel, opts = {}) {
42991
42991
  return this.request("POST", "/api/v1/business-context/remove-voice-source", { projectId, channel, ...opts });
42992
42992
  }
42993
- refineBrandVoice(projectId, source, correction) {
42994
- return this.request("POST", "/api/v1/business-context/refine-brand-voice", { projectId, ...source && { source }, ...correction && { correction } });
42993
+ refineBrandVoice(projectId, source, correction, scope) {
42994
+ return this.request("POST", "/api/v1/business-context/refine-brand-voice", { projectId, ...source && { source }, ...correction && { correction }, ...scope && { scope } });
42995
42995
  }
42996
42996
  getProfile() {
42997
42997
  return this.request("GET", "/api/v1/users/profile");
@@ -43701,6 +43701,7 @@ function qualifiedModelIds(listing, filter) {
43701
43701
  var HOBBY_ALLOWED = new Set([
43702
43702
  "anthropic/claude-sonnet-5.5",
43703
43703
  "anthropic/claude-sonnet-5",
43704
+ "anthropic/claude-haiku-5.5",
43704
43705
  "anthropic/claude-haiku-4.5"
43705
43706
  ]);
43706
43707
  var USAGE_WINDOWS = {
@@ -43836,6 +43837,53 @@ function getMimeTypeFromFilename(filename) {
43836
43837
  const ext = filename.toLowerCase().split(".").pop();
43837
43838
  return EXT_TO_MIME[ext || ""] || "";
43838
43839
  }
43840
+ function truncatedImageReason(data) {
43841
+ const n = data.length;
43842
+ const ascii = (i, len) => String.fromCharCode(...data.subarray(i, i + len));
43843
+ if (n >= 4 && data[0] === 255 && data[1] === 216 && data[2] === 255) {
43844
+ let i = 2;
43845
+ while (i + 1 < n) {
43846
+ if (data[i] !== 255)
43847
+ return;
43848
+ const marker = data[i + 1];
43849
+ if (marker === 217)
43850
+ return;
43851
+ if (marker === 255) {
43852
+ i++;
43853
+ continue;
43854
+ }
43855
+ if (marker === 1 || marker >= 208 && marker <= 216) {
43856
+ i += 2;
43857
+ continue;
43858
+ }
43859
+ if (i + 3 >= n)
43860
+ break;
43861
+ i += 2 + (data[i + 2] << 8 | data[i + 3]);
43862
+ if (marker === 218) {
43863
+ while (i + 1 < n && !(data[i] === 255 && data[i + 1] !== 0 && !(data[i + 1] >= 208 && data[i + 1] <= 215)))
43864
+ i++;
43865
+ }
43866
+ }
43867
+ return "JPEG ends before its end-of-image marker (FF D9)";
43868
+ }
43869
+ if (n >= 8 && ascii(0, 8) === `‰PNG\r
43870
+ \x1A
43871
+ `) {
43872
+ let i = 8;
43873
+ while (i + 8 <= n) {
43874
+ const len = (data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]) >>> 0;
43875
+ if (ascii(i + 4, 4) === "IEND")
43876
+ return i + 12 <= n ? undefined : "PNG ends inside its IEND chunk";
43877
+ i += 12 + len;
43878
+ }
43879
+ return "PNG ends before its IEND chunk";
43880
+ }
43881
+ if (n >= 12 && ascii(0, 4) === "RIFF" && ascii(8, 4) === "WEBP") {
43882
+ const riff = (data[4] | data[5] << 8 | data[6] << 16 | data[7] << 24) >>> 0;
43883
+ return n >= riff + 8 ? undefined : `WebP RIFF header declares ${riff + 8} bytes, file has ${n}`;
43884
+ }
43885
+ return;
43886
+ }
43839
43887
  var NEEDS_OBJECT_URL = new Set(Object.entries(mimetypes_default.types).filter(([, meta]) => meta.needsObjectUrl).map(([type]) => type));
43840
43888
  var MIME_TO_EXT = {};
43841
43889
  for (const [ext, entry] of Object.entries(mimetypes_default.extensions)) {
@@ -45358,7 +45406,7 @@ var tool_catalog_default = {
45358
45406
  preinstallCloud: true,
45359
45407
  label: "Browser",
45360
45408
  summary: "Browse websites automatically: click, type, read and screenshot.",
45361
- description: "Browser automation over CDP (`open`/`snapshot`/`click`/`type`/`fill`/`read`/`screenshot`/`eval`). `connect '<url>'` to a listed live browser (prefer the user's real Chrome), verify with `snapshot`; if empty, `close` (own session only) and reconnect once. NEVER `close --all`. Shared browser: `tab new` for your own tab, `tab <id>` before each command. No URL listed: cloud → browser-manager-cli; PC with display → Chrome `--remote-debugging-port=9222`, attach `--cdp 9222`.",
45409
+ description: "Browser automation over CDP (`open`/`snapshot`/`click`/`type`/`fill`/`read`/`screenshot`/`eval`). `connect '<url>'` to a listed live browser (prefer the user's real Chrome), verify with `snapshot`; if empty, `close` (own session only) and reconnect once. NEVER `close --all`. Shared browser: `tab new` for your own tab, `tab <id>` before each command. No URL listed: cloud → browser-manager-cli; PC with display → Chrome `--remote-debugging-port=9222`, attach `--cdp 9222`. Never `sleep`: `open` returns after load, but `click`/`fill` fail at once if the element isn't there yet — `wait '<sel>'`, `wait --text 'Done'`, `wait --url '**/dash'`, `wait --load networkidle` or `wait '#spinner' --state hidden` first.",
45362
45410
  versionCmd: "agent-browser --version 2>/dev/null | sed -n 1p",
45363
45411
  preinstallJarvis: true
45364
45412
  },
@@ -45903,6 +45951,7 @@ var tool_catalog_default = {
45903
45951
  summary: "Post to your Facebook Page and its linked Instagram account, and handle comments.",
45904
45952
  statusCmd: `facebook-pages-api whoami 2>/dev/null | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
45905
45953
  loginCmd: "facebook-pages-api auth --page-id <id> --page-token <token>",
45954
+ oauthBroker: true,
45906
45955
  credentialPaths: ["~/.config/facebook-pages-api/credentials.json"],
45907
45956
  description: "Facebook Pages CLI (Meta Graph API). `auth --page-id <id> --page-token <token>` (or env FB_PAGE_ID/FB_PAGE_TOKEN). `feed`, `comments`, `insights`, `conversations`, `media`, and `request <GET|POST|DELETE> <path> [--data <json>]` = any Graph edge with the page token attached (use instead of curl). Writes are live: `feed create --published false` for tests; `comments reply` posts publicly — get human approval. Follower totals: `whoami --fields followers_count,fan_count` (many classic insights metrics are retired). Instagram (linked professional account, same page token): `ig whoami|media|get|stories|tags|limit`, `ig publish --image_url U | --video_url U [--media_type STORIES] | --carousel U1,U2 [--caption …]` (public URLs; container → wait → publish; `--create-only` = dry run; 100 posts/24h), `ig comments list|replies|create|reply|hide|delete|toggle`, `ig insights [media_id]` (needs instagram_manage_insights). `--help` for subcommands.",
45908
45957
  versionCmd: "facebook-pages-api --version 2>/dev/null | sed -n 1p"
@@ -50151,7 +50200,7 @@ var package_default = {
50151
50200
  type: "git",
50152
50201
  url: "git+https://github.com/todoforai/cli.git"
50153
50202
  },
50154
- version: "0.1.65",
50203
+ version: "0.1.66",
50155
50204
  type: "module",
50156
50205
  bin: {
50157
50206
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -50251,6 +50300,7 @@ Options:
50251
50300
  --no-bridge Do not auto-spawn bridge
50252
50301
  --json
50253
50302
  --detailed | --format-anthropic inspect --json: keep ids/timestamps | Anthropic messages shape
50303
+ --full inspect: no truncation of text, thinking, tool calls and results
50254
50304
  --safe Validate API key upfront
50255
50305
  --debug, -d
50256
50306
  --debug-dump Attach LLM request debug info per turn (requires server grant)
@@ -50263,8 +50313,9 @@ tfa-cli show — put a file or url into the chat
50263
50313
 
50264
50314
  Usage:
50265
50315
  tfa-cli show <file|-> [todo-id] Render a file by mimetype (- reads stdin)
50266
- [--title T] [--alias A] [--mime M] [--card <name>] [--link] [--json]
50316
+ [--title T] [--alias A] [--mime M] [--card <name>] [--link] [--force] [--json]
50267
50317
  --link: compact download chip instead of inline render
50318
+ Refuses a likely-truncated image (JPEG/PNG cut before the end marker, WebP shorter than its RIFF size); --force shows it anyway
50268
50319
  Prints "<todoId>:<alias|id>". Private: visible to whoever can read the todo.
50269
50320
  Re-showing with the same --alias updates the block in place (new version)
50270
50321
  --alias <todoId>:<alias> updates that todo's block instead,
@@ -50342,10 +50393,12 @@ function parseCliArgs() {
50342
50393
  mime: { type: "string" },
50343
50394
  card: { type: "string" },
50344
50395
  link: { type: "boolean", default: false },
50396
+ force: { type: "boolean", default: false },
50345
50397
  direction: { type: "string" },
50346
50398
  url: { type: "string" },
50347
50399
  account: { type: "string" },
50348
50400
  "from-company": { type: "boolean", default: false },
50401
+ scope: { type: "string" },
50349
50402
  "dry-run": { type: "boolean", default: false },
50350
50403
  max: { type: "string" },
50351
50404
  seed: { type: "string" },
@@ -50370,6 +50423,7 @@ function parseCliArgs() {
50370
50423
  "no-edge": { type: "boolean", default: false },
50371
50424
  json: { type: "boolean", default: false },
50372
50425
  detailed: { type: "boolean", default: false },
50426
+ full: { type: "boolean", default: false },
50373
50427
  "format-anthropic": { type: "boolean", default: false },
50374
50428
  safe: { type: "boolean", default: false },
50375
50429
  debug: { type: "boolean", short: "d", default: false },
@@ -51268,7 +51322,8 @@ function getFrontendUrl(apiUrl, todoId) {
51268
51322
  }
51269
51323
 
51270
51324
  // src/inspect.ts
51271
- var xmlAttr = (v) => JSON.stringify(String(v));
51325
+ var str = (v) => typeof v === "string" ? v : JSON.stringify(v);
51326
+ var xmlAttr = (v) => JSON.stringify(str(v));
51272
51327
  var DROP_IF_EMPTY = new Set(["runMeta", "meta", "attachments", "blocks"]);
51273
51328
  var ALWAYS_DROP = new Set(["messageIds", "data"]);
51274
51329
  var DETAILED_ONLY = new Set([
@@ -51494,12 +51549,14 @@ function applySlice(arr, spec) {
51494
51549
  throw new Error(`Bad slice: '${spec}'`);
51495
51550
  return arr.slice(start, end);
51496
51551
  }
51497
- var trunc = (s, n) => s.length > n ? s.slice(0, n) + `
51498
- ${DIM}... (${s.length} chars)${RESET}` : s;
51552
+ var truncAt = (s, n) => s.length > n ? s.slice(0, n) + `
51553
+ ${DIM}... (${s.length} chars, --full for all)${RESET}` : s;
51499
51554
  var indent = (s, pre) => s.split(`
51500
51555
  `).join(`
51501
51556
  ${pre}`);
51502
- function printFullChat(todo, frontendUrl, slice, mode = "default", format = "compact") {
51557
+ function printFullChat(todo, frontendUrl, slice, mode = "default", format = "compact", full = false) {
51558
+ const out = (s) => process.stdout.write(s);
51559
+ const trunc = (s, n) => full ? s : truncAt(s, n);
51503
51560
  const statusColors = {
51504
51561
  DONE: GREEN,
51505
51562
  READY: GREEN,
@@ -51513,34 +51570,34 @@ function printFullChat(todo, frontendUrl, slice, mode = "default", format = "com
51513
51570
  TODO: CYAN
51514
51571
  };
51515
51572
  const statusColor = statusColors[todo.status] || DIM;
51516
- process.stderr.write(`${BOLD}TODO${RESET} ${todo.id}
51573
+ out(`${BOLD}TODO${RESET} ${todo.id}
51517
51574
  `);
51518
- process.stderr.write(`${DIM}Status:${RESET} ${statusColor}${todo.status}${RESET}
51575
+ out(`${DIM}Status:${RESET} ${statusColor}${todo.status}${RESET}
51519
51576
  `);
51520
- process.stderr.write(`${DIM}URL:${RESET} ${CYAN}${frontendUrl}${RESET}
51577
+ out(`${DIM}URL:${RESET} ${CYAN}${frontendUrl}${RESET}
51521
51578
  `);
51522
- process.stderr.write(`${DIM}Created:${RESET} ${new Date(todo.createdAt).toLocaleString()}
51579
+ out(`${DIM}Created:${RESET} ${new Date(todo.createdAt).toLocaleString()}
51523
51580
  `);
51524
51581
  if (todo.agentSettingsId)
51525
- process.stderr.write(`${DIM}Agent:${RESET} ${todo.agentSettingsId}
51582
+ out(`${DIM}Agent:${RESET} ${todo.agentSettingsId}
51526
51583
  `);
51527
51584
  if (slice)
51528
- process.stderr.write(`${DIM}Slice:${RESET} [${slice}]
51585
+ out(`${DIM}Slice:${RESET} [${slice}]
51529
51586
  `);
51530
- process.stderr.write("─".repeat(60) + `
51587
+ out("─".repeat(60) + `
51531
51588
  `);
51532
51589
  let messages = todo.messages || [];
51533
51590
  if (slice) {
51534
51591
  try {
51535
51592
  messages = applySlice(messages, slice);
51536
51593
  } catch (e) {
51537
- process.stderr.write(`${RED}${e.message}${RESET}
51594
+ out(`${RED}${e.message}${RESET}
51538
51595
  `);
51539
51596
  process.exit(2);
51540
51597
  }
51541
51598
  }
51542
51599
  if (!messages.length) {
51543
- process.stderr.write(`${DIM}(no messages)${RESET}
51600
+ out(`${DIM}(no messages)${RESET}
51544
51601
  `);
51545
51602
  return;
51546
51603
  }
@@ -51551,33 +51608,33 @@ function printFullChat(todo, frontendUrl, slice, mode = "default", format = "com
51551
51608
  const orig = messages[i];
51552
51609
  const ts = mode !== "default" && orig?.createdAt ? ` ${DIM}${new Date(orig.createdAt).toLocaleTimeString()}${RESET}` : "";
51553
51610
  const label = msg.role === "user" ? `${CYAN}▶ USER${RESET}` : `${GREEN}◀ ASSISTANT${RESET}`;
51554
- process.stderr.write(`
51611
+ out(`
51555
51612
  ${label}${ts}
51556
51613
  `);
51557
51614
  const items = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
51558
51615
  for (const it of items) {
51559
51616
  if (it.type === "text") {
51560
51617
  if (it.text)
51561
- process.stdout.write(trunc(it.text, 2000) + `
51618
+ out(trunc(it.text, 2000) + `
51562
51619
  `);
51563
51620
  } else if (it.type === "image" || it.type === "document") {
51564
51621
  const s = it.source ?? {};
51565
- process.stderr.write(` ${YELLOW}[${it.type}]${RESET} ${DIM}${s.mimeType ?? ""}${RESET} ${s.name ?? ""} ${DIM}(${s.size ?? "?"} bytes)${RESET}
51622
+ out(` ${YELLOW}[${it.type}]${RESET} ${DIM}${s.mimeType ?? ""}${RESET} ${s.name ?? ""} ${DIM}(${s.size ?? "?"} bytes)${RESET}
51566
51623
  ${DIM}${s.uri ?? ""}${RESET}
51567
51624
  `);
51568
51625
  } else if (it.type === "thinking") {
51569
51626
  if (it.thinking)
51570
- process.stderr.write(`${DIM}[thinking]${RESET} ${trunc(it.thinking, 500)}
51627
+ out(`${DIM}[thinking]${RESET} ${trunc(it.thinking, 500)}
51571
51628
  `);
51572
51629
  } else if (it.type === "tool_use") {
51573
51630
  toolUseCount++;
51574
51631
  if (it.content) {
51575
- process.stderr.write(` ${YELLOW}${trunc(it.content, 200)}${RESET}
51632
+ out(` ${YELLOW}${trunc(it.content, 200)}${RESET}
51576
51633
  `);
51577
51634
  } else {
51578
- const argStr = Object.entries(it.input || {}).map(([k, v]) => `${DIM}${k}=${RESET}${String(v).split(`
51635
+ const argStr = Object.entries(it.input || {}).map(([k, v]) => `${DIM}${k}=${RESET}${full ? str(v) : str(v).split(`
51579
51636
  `)[0].slice(0, 80)}`).join(" ");
51580
- process.stderr.write(` ${YELLOW}[${it.name}]${RESET} ${argStr}
51637
+ out(` ${YELLOW}[${it.name}]${RESET} ${argStr}
51581
51638
  `);
51582
51639
  }
51583
51640
  } else if (it.type === "tool_result") {
@@ -51600,15 +51657,15 @@ ${label}${ts}
51600
51657
  }).join(`
51601
51658
  `);
51602
51659
  }
51603
- process.stderr.write(` ${DIM}└─ result:${RESET}${status} ${indent(trunc(bodyStr, 500), ` ${DIM}│${RESET} `)}
51660
+ out(` ${DIM}└─ result:${RESET}${status} ${indent(trunc(bodyStr, 500), ` ${DIM}│${RESET} `)}
51604
51661
  `);
51605
51662
  }
51606
51663
  }
51607
51664
  }
51608
- process.stderr.write(`
51665
+ out(`
51609
51666
  ` + "─".repeat(60) + `
51610
51667
  `);
51611
- process.stderr.write(`${DIM}Messages: ${shaped.length} | Tool calls: ${toolUseCount} | Tool errors: ${errorCount}${RESET}
51668
+ out(`${DIM}Messages: ${shaped.length} | Tool calls: ${toolUseCount} | Tool errors: ${errorCount}${RESET}
51612
51669
  `);
51613
51670
  }
51614
51671
 
@@ -53823,6 +53880,12 @@ Usage:
53823
53880
  tfa-cli brand voice correct "<what is off>" Tell the learner what it got wrong; profile is updated
53824
53881
  in one pass and the correction is kept for every re-learn
53825
53882
 
53883
+ Every voice verb takes --scope project|mine|personal — which voice to teach:
53884
+ project the project's, shared with its members (default; needs write access)
53885
+ mine yours, only in this project
53886
+ personal yours, in every project (e.g. your own Gmail)
53887
+ Agents read all three; on the same voice name the narrower wins: mine > project > personal.
53888
+
53826
53889
 
53827
53890
  Questions the UI asks (keys for 'answers'):
53828
53891
  "What makes you different from competitors?"
@@ -53921,10 +53984,13 @@ async function voiceDeviceCommand(rest, args) {
53921
53984
  }
53922
53985
  async function voiceCommand(api, projectId, rest, args) {
53923
53986
  const [verb, ...vargs] = rest;
53987
+ const scope = args.scope;
53988
+ if (scope && !["project", "mine", "personal"].includes(scope))
53989
+ fail(`--scope must be project, mine or personal, got '${scope}'`);
53924
53990
  if (verb === "collect" && !vargs[0])
53925
53991
  fail("Usage: tfa-cli brand voice collect <channel> [--url U | --account <page-id> | --max N] [--dry-run]");
53926
53992
  if (verb === "answers") {
53927
- const answers = manualAnswersOf((await api.listVoiceSources(projectId)).sources);
53993
+ const answers = manualAnswersOf((await api.listVoiceSources(projectId, scope)).sources);
53928
53994
  if (vargs.length) {
53929
53995
  for (const a of vargs) {
53930
53996
  const i = a.indexOf("=");
@@ -53932,7 +53998,7 @@ async function voiceCommand(api, projectId, rest, args) {
53932
53998
  fail(`Expected question=answer, got '${a}'`);
53933
53999
  answers[a.slice(0, i)] = a.slice(i + 1);
53934
54000
  }
53935
- await api.collectVoiceSource(projectId, MANUAL_CHANNEL, { samples: manualSamples(answers) });
54001
+ await api.collectVoiceSource(projectId, MANUAL_CHANNEL, { samples: manualSamples(answers), scope });
53936
54002
  process.stderr.write(`${GREEN}✅ ${vargs.length} answer(s) saved${RESET}
53937
54003
  `);
53938
54004
  return;
@@ -53948,7 +54014,7 @@ async function voiceCommand(api, projectId, rest, args) {
53948
54014
  return;
53949
54015
  }
53950
54016
  if (!verb || verb === "show") {
53951
- const [{ sources, profile }, brand] = await Promise.all([api.listVoiceSources(projectId), api.getBusinessContext(projectId)]);
54017
+ const [{ sources, profile }, brand] = await Promise.all([api.listVoiceSources(projectId, scope), api.getBusinessContext(projectId)]);
53952
54018
  if (args.json) {
53953
54019
  console.log(JSON.stringify({ brand, profile, sources }, null, 2));
53954
54020
  return;
@@ -53979,12 +54045,12 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
53979
54045
  const { samples, note } = collectChannel(channel, Number(args.max ?? 40));
53980
54046
  if (!samples.length)
53981
54047
  fail(`${channel}: nothing usable came back${note ? ` (${note})` : ""}`);
53982
- const { sources: sources2 } = await api.collectVoiceSource(projectId, channel, { samples });
54048
+ const { sources: sources2 } = await api.collectVoiceSource(projectId, channel, { samples, scope });
53983
54049
  process.stderr.write(`${GREEN}✅ ${channel}: ${samples.length} reply pairs stored (${sources2.length} source(s))${RESET}
53984
54050
  `);
53985
54051
  return;
53986
54052
  }
53987
- const { sources } = await api.collectVoiceSource(projectId, channel, { url: args.url, account: args.account });
54053
+ const { sources } = await api.collectVoiceSource(projectId, channel, { url: args.url, account: args.account, scope });
53988
54054
  process.stderr.write(`${GREEN}✅ ${channel} collected (${sources.length} source(s))${RESET}
53989
54055
  `);
53990
54056
  return;
@@ -53992,7 +54058,7 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
53992
54058
  if (verb === "remove") {
53993
54059
  if (!vargs[0])
53994
54060
  fail("Usage: tfa-cli brand voice remove <channel> [--account <id>] (no --account: every source of the channel)");
53995
- await api.removeVoiceSource(projectId, vargs[0], args.account ? { account: args.account } : { all: true });
54061
+ await api.removeVoiceSource(projectId, vargs[0], { ...args.account ? { account: args.account } : { all: true }, scope });
53996
54062
  process.stderr.write(`${GREEN}✅ ${vargs[0]} removed${RESET}
53997
54063
  `);
53998
54064
  return;
@@ -54000,7 +54066,7 @@ ${DIM}match ${profile.match}/100 · source ${profile.source}${RESET}
54000
54066
  if (verb === "learn") {
54001
54067
  process.stderr.write(`${DIM}learning voice…${RESET}
54002
54068
  `);
54003
- const res = await api.refineBrandVoice(projectId, args["from-company"] ? "company" : undefined);
54069
+ const res = await api.refineBrandVoice(projectId, args["from-company"] ? "company" : undefined, undefined, scope);
54004
54070
  if (!res.profile)
54005
54071
  fail(args["from-company"] ? "No brand page to learn from — 'tfa-cli brand create <name>' and fill it first" : "Nothing to learn from — add answers, collect a source, or fill the brand page first");
54006
54072
  if (args.json) {
@@ -54016,11 +54082,11 @@ ${res.profile}
54016
54082
  const text = vargs.join(" ").trim();
54017
54083
  if (!text)
54018
54084
  fail('Usage: tfa-cli brand voice correct "<what is off>"');
54019
- if (!(await api.listVoiceSources(projectId)).profile?.profile)
54085
+ if (!(await api.listVoiceSources(projectId, scope)).profile?.profile)
54020
54086
  fail("No voice learned yet — 'tfa-cli brand voice learn' first");
54021
54087
  process.stderr.write(`${DIM}applying correction…${RESET}
54022
54088
  `);
54023
- const res = await api.refineBrandVoice(projectId, undefined, text);
54089
+ const res = await api.refineBrandVoice(projectId, undefined, text, scope);
54024
54090
  if (!res.profile)
54025
54091
  fail("The correction pass returned nothing — try rewording it");
54026
54092
  if (args.json) {
@@ -55182,21 +55248,24 @@ tfa-cli memory — memory and context folders as git checkouts
55182
55248
 
55183
55249
  Usage:
55184
55250
  tfa-cli memory sync [--project <id>] Clone, or pull, into:
55185
- ~/.todoforai/{memory,context} yours (todoforai:memory, todoforai:context)
55186
- ~/.todoforai/projects/<id>/{memory,context} the project's (todoforai:projects/<id>/…)
55251
+ ~/.todoforai/{memory,context} yours (todoforai:memory, todoforai:context)
55252
+ ~/.todoforai/projects/<id>/{memory,context} the project's (todoforai:projects/<id>/…)
55253
+ ~/.todoforai/projects/<id>/me/{memory,context} yours in it (todoforai:projects/<id>/me/…)
55187
55254
  tfa-cli memory path [--project <id>] Print the checkout paths
55188
55255
 
55189
55256
  Every change to these folders is committed on the server, from any client. In a checkout
55190
55257
  use plain git: log, diff, show, revert, commit, then \`git push\` — the push updates the live
55191
55258
  folder. A push behind the server is refused: \`git pull\` first. Files over 5 MB live outside
55192
- history (upload them instead). Project folders: members with write access push. The project
55259
+ history (upload them instead). Project folders: members with write access push; your own in
55260
+ a project (me/) is private to you. The project
55193
55261
  defaults to $TODOFORAI_PROJECT_ID.
55194
55262
  `);
55195
55263
  }
55196
55264
  var FOLDERS = ["memory", "context"];
55197
55265
  function checkouts(projectId) {
55198
55266
  const root = join5(homedir5(), ".todoforai");
55199
- const scopes = [[root, ""], ...projectId ? [[join5(root, "projects", projectId), `projects/${projectId}/`]] : []];
55267
+ const project = projectId && join5(root, "projects", projectId);
55268
+ const scopes = [[root, ""], ...project ? [[project, `projects/${projectId}/`], [join5(project, "me"), `projects/${projectId}/me/`]] : []];
55200
55269
  return scopes.flatMap(([dir, remote]) => FOLDERS.map((f) => [join5(dir, f), `${remote}${f}.git`]));
55201
55270
  }
55202
55271
  function git(args, cwd) {
@@ -56399,14 +56468,14 @@ function promiseAllObject(promisesObj) {
56399
56468
  }
56400
56469
  function randomString(length = 10) {
56401
56470
  const chars = "abcdefghijklmnopqrstuvwxyz";
56402
- let str = "";
56471
+ let str2 = "";
56403
56472
  for (let i = 0;i < length; i++) {
56404
- str += chars[Math.floor(Math.random() * chars.length)];
56473
+ str2 += chars[Math.floor(Math.random() * chars.length)];
56405
56474
  }
56406
- return str;
56475
+ return str2;
56407
56476
  }
56408
- function esc(str) {
56409
- return JSON.stringify(str);
56477
+ function esc(str2) {
56478
+ return JSON.stringify(str2);
56410
56479
  }
56411
56480
  function slugify(input) {
56412
56481
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -56519,8 +56588,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
56519
56588
  "symbol",
56520
56589
  "undefined"
56521
56590
  ]);
56522
- function escapeRegex(str) {
56523
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56591
+ function escapeRegex(str2) {
56592
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56524
56593
  }
56525
56594
  function clone(inst, def, params) {
56526
56595
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -56771,13 +56840,13 @@ function getSizableOrigin(input) {
56771
56840
  return "unknown";
56772
56841
  }
56773
56842
  var highSurrogate = /[\uD800-\uDBFF]/;
56774
- function codePointLength(str) {
56775
- const units = str.length;
56776
- if (!highSurrogate.test(str))
56843
+ function codePointLength(str2) {
56844
+ const units = str2.length;
56845
+ if (!highSurrogate.test(str2))
56777
56846
  return units;
56778
56847
  let count = units;
56779
56848
  for (let i = 0;i < units - 1; i++) {
56780
- if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
56849
+ if ((str2.charCodeAt(i) & 64512) === 55296 && (str2.charCodeAt(i + 1) & 64512) === 56320) {
56781
56850
  count--;
56782
56851
  i++;
56783
56852
  }
@@ -80266,17 +80335,17 @@ async function main() {
80266
80335
  const [, filePath, todoArg] = positionals;
80267
80336
  const todoId = todoArg || aliasTodoId(args.alias) || getEnv("TODO_ID") || cfgScope.data.last_todo_id;
80268
80337
  if (!filePath || !todoId) {
80269
- process.stderr.write(`${RED3}Usage: tfa-cli show <file|-> [todo-id] [--title T] [--alias A] [--mime M] [--card <name>] [--link]${RESET2}
80338
+ process.stderr.write(`${RED3}Usage: tfa-cli show <file|-> [todo-id] [--title T] [--alias A] [--mime M] [--card <name>] [--link] [--force]${RESET2}
80270
80339
  `);
80271
80340
  process.exit(2);
80272
80341
  }
80273
- let blob, name;
80342
+ let bytes, name, mime;
80274
80343
  if (filePath === "-") {
80275
80344
  const chunks = [];
80276
80345
  for await (const c3 of process.stdin)
80277
80346
  chunks.push(c3);
80278
- blob = new Blob([Buffer.concat(chunks)]);
80279
- if (blob.size === 0) {
80347
+ bytes = Buffer.concat(chunks);
80348
+ if (bytes.length === 0) {
80280
80349
  process.stderr.write(`${RED3}No data on stdin${RESET2}
80281
80350
  `);
80282
80351
  process.exit(1);
@@ -80289,9 +80358,21 @@ async function main() {
80289
80358
  `);
80290
80359
  process.exit(1);
80291
80360
  }
80292
- blob = new Blob([readFileSync5(abs)], { type: getMimeTypeFromFilename(abs) || undefined });
80361
+ bytes = readFileSync5(abs);
80362
+ mime = getMimeTypeFromFilename(abs) || undefined;
80293
80363
  name = path4.basename(filePath);
80294
80364
  }
80365
+ const truncated = truncatedImageReason(bytes);
80366
+ if (truncated) {
80367
+ if (!args.force) {
80368
+ process.stderr.write(`${RED3}${name} looks truncated: ${truncated} (${bytes.length} bytes). Re-export it, or pass --force to show it anyway.${RESET2}
80369
+ `);
80370
+ process.exit(1);
80371
+ }
80372
+ process.stderr.write(`${YELLOW2}\u26A0 ${name} looks truncated: ${truncated} \u2014 showing anyway (--force)${RESET2}
80373
+ `);
80374
+ }
80375
+ const blob = new Blob([bytes], { type: mime });
80295
80376
  const res = await api2.showFile(todoId, blob, name, {
80296
80377
  title: args.title,
80297
80378
  alias: args.alias,
@@ -80494,7 +80575,7 @@ async function main() {
80494
80575
  }
80495
80576
  const mode = args.debug ? "debug" : args.detailed ? "detailed" : "default";
80496
80577
  const format = args["format-anthropic"] ? "anthropic" : "compact";
80497
- printFullChat(todo2, getFrontendUrl(apiUrl, todoId), slice, mode, format);
80578
+ printFullChat(todo2, getFrontendUrl(apiUrl, todoId), slice, mode, format, args.full);
80498
80579
  return;
80499
80580
  }
80500
80581
  if (process.stderr.isTTY)
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/todoforai/cli.git"
8
8
  },
9
- "version": "0.1.65",
9
+ "version": "0.1.66",
10
10
  "type": "module",
11
11
  "bin": {
12
12
  "todoforai-cli": "bin/todoforai-cli.js",