@todoforai/cli 0.1.19 → 0.1.21

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/todoai.js +390 -45
  2. package/package.json +2 -1
package/dist/todoai.js CHANGED
@@ -42825,6 +42825,9 @@ class ApiClient {
42825
42825
  deleteTodo(todoId) {
42826
42826
  return this.request("DELETE", `/api/v1/todos/${todoId}`);
42827
42827
  }
42828
+ listModels() {
42829
+ return this.request("GET", "/api/v1/models");
42830
+ }
42828
42831
  listAgentSettings(filters) {
42829
42832
  const params = new URLSearchParams;
42830
42833
  if (filters?.workspacePath)
@@ -42923,7 +42926,7 @@ function normalizeApiUrl(url) {
42923
42926
  return `https://${url}`;
42924
42927
  return url;
42925
42928
  }
42926
- var SUBCOMMANDS = new Set(["login", "logout"]);
42929
+ var SUBCOMMANDS = new Set(["login", "logout", "update"]);
42927
42930
  function credentialsPath() {
42928
42931
  const sys = os2.platform();
42929
42932
  if (sys === "win32") {
@@ -43147,6 +43150,7 @@ var package_default = {
43147
43150
  postinstall: "rm -rf node_modules/@todoforai/edge && ln -s ../../../edge/bun node_modules/@todoforai/edge"
43148
43151
  },
43149
43152
  dependencies: {
43153
+ "@shared/credentials": "file:../packages/shared-credentials",
43150
43154
  "@todoforai/update-notifier": "^0.1.0",
43151
43155
  "cli-highlight": "^2.1.11",
43152
43156
  "diff-match-patch": "^1.0.5",
@@ -43336,7 +43340,8 @@ var NON_SHELL_TYPES = new Set([
43336
43340
  var USER_INPUT_BLOCK_TYPES = new Set([
43337
43341
  "question" /* Question */,
43338
43342
  "login_prompt" /* Login */,
43339
- "business_onboarding" /* BusinessOnboarding */
43343
+ "business_onboarding" /* BusinessOnboarding */,
43344
+ "open_browser" /* OpenBrowser */
43340
43345
  ]);
43341
43346
  var EDGE_TOOL_TYPES = new Set([
43342
43347
  "read" /* Read */,
@@ -43363,7 +43368,6 @@ var API_TOOL_TYPES = new Set([
43363
43368
  "api_get_agent" /* ApiGetAgent */,
43364
43369
  "api_update_agent" /* ApiUpdateAgent */,
43365
43370
  "api_list_business_contexts" /* ApiListBusinessContexts */,
43366
- "api_get_business_context" /* ApiGetBusinessContext */,
43367
43371
  "api_get_context_item" /* ApiGetContextItem */,
43368
43372
  "api_create_context_item" /* ApiCreateContextItem */,
43369
43373
  "api_update_context_item" /* ApiUpdateContextItem */,
@@ -43396,6 +43400,9 @@ var SERVER_TO_FRONTENDS = {
43396
43400
  hexGrid: {
43397
43401
  updated: (projectId) => `${"hexgrid:updated" /* HEXGRID_UPDATED */}:${projectId}`
43398
43402
  },
43403
+ recommendation: {
43404
+ updated: (projectId) => `${"recommendation:updated" /* RECOMMENDATION_UPDATED */}:${projectId}`
43405
+ },
43399
43406
  sandbox: {
43400
43407
  state: (userId) => `${"sandbox:state" /* SANDBOX_STATE */}:${userId}`
43401
43408
  },
@@ -43839,15 +43846,70 @@ Cover these sections (skip any without data):
43839
43846
 
43840
43847
  If anything important remains unclear, add an **Open Questions** section at the end.
43841
43848
 
43849
+ Maintain a single machine-readable **Growth Plan** block so the user's brand page can render a live presence map and a ranked, actionable growth plan. Write it as one fenced code block tagged \`growth-plan\` containing JSON (the user never sees this raw \u2014 it powers the presence hexes and the Growth Plan panel). Keep exactly one such block; rewrite it in full each time you update it. Concrete example (emit strictly valid JSON like this \u2014 no comments, no ranges, no "|" alternatives):
43850
+
43851
+ \`\`\`growth-plan
43852
+ {
43853
+ "presence": {
43854
+ "linkedin-networking": { "score": 45, "handle": "https://linkedin.com/company/example", "note": "Company page exists but posts infrequently" },
43855
+ "x-twitter-presence": { "score": 60, "handle": "@example", "note": "Active, regular posts" },
43856
+ "newsletter": { "score": null, "note": "No newsletter signup found" }
43857
+ },
43858
+ "recommendations": [
43859
+ {
43860
+ "activityId": "linkedin-networking",
43861
+ "title": "Publish two founder-led LinkedIn posts each week",
43862
+ "rationale": "Target buyers are active on LinkedIn, but the page is nearly dormant.",
43863
+ "angle": "Explain the operational pain through short customer scenarios.",
43864
+ "track": "Qualified profile visits and demo requests per week",
43865
+ "priority": "high"
43866
+ }
43867
+ ]
43868
+ }
43869
+ \`\`\`
43870
+
43871
+ Rules for the Growth Plan block:
43872
+ - \`activityId\` MUST be one of these exact ids: facebook-posting, x-twitter-presence, tiktok-content, instagram-content, linkedin-networking, reddit-engagement, blog-writing, seo-optimization, backlink-building, keyword-research, aso-optimization, llm-optimization, google-ads-management, meta-ads-management, lead-outreach, crm-management, newsletter, affiliate-program, partnership-outreach, analytics-monitoring.
43873
+ - \`score\` is an integer 0-100, or \`null\` when you checked but found no presence. Base scores on real findings, not guesses. Include every channel you actually checked.
43874
+ - \`recommendations\`: 3-6 items. \`priority\` is exactly one of "high", "medium", "low"; put the single highest-leverage action first. Choose channels that fit THIS business's model, audience, and stage \u2014 don't recommend all 20; prioritize gaps where presence is weak/absent but impact is high. Make \`title\` a concrete next action, \`angle\` the actual message/positioning to use, and \`track\` a specific metric or cadence to record.
43875
+ - Keep it consistent with the prose Online Presence and Next Steps sections \u2014 the JSON is the structured mirror of that advice.
43876
+
43877
+ Document order for the saved brand page: human-readable sections first, then the **Next Steps** section, then Open Questions (if any), and finally exactly one \`growth-plan\` block as the very last element. (Your chat reply can still end with a short spoken summary; only the saved markdown must end with the machine block.)
43878
+
43842
43879
  Guide the user one step at a time \u2014 they should always know the single next thing to do:
43843
43880
  - When you need input (brand voice, tone, target audience, which channel to prioritize), prefer the question tool with short multi-choice options over asking open-ended prose questions.
43844
43881
  - After completing any analysis or context update, always close with a short **Next Steps** section: 2-4 concrete, prioritized tips tailored to this specific business (e.g. SEO/content gaps, social channels worth activating, positioning against the competitors you found, conversion or messaging improvements). Base tips on what you actually observed, keep each to one or two sentences, and lead with the one you'd do first.
43845
43882
 
43846
- In follow-up conversations, act as an ongoing advisor: answer growth and digital-presence questions using the business context, refresh your analysis when asked, and suggest what to tackle next.`,
43883
+ In follow-up conversations, act as an ongoing advisor: answer growth and digital-presence questions using the business context, refresh your analysis when asked, and suggest what to tackle next.
43884
+
43885
+ When the user asks you to recommend how to grow (e.g. "recommend growth TODOs", the brand page's guided suggestion), treat it as a growth-plan request: read the current business context (including any existing \`growth-plan\` block), research only where information is missing or stale, then update the business context with a refreshed Growth Plan block and reply in chat with the same ranked recommendations in plain language \u2014 lead with the single highest-leverage action and why. This must work at any point in the conversation, not just after a full website analysis.
43886
+
43887
+ Recommending actionable TODOs from your recommendations:
43888
+ When the user asks you to generate/recommend TODOs (e.g. "generate TODOs", the board's Generate button), turn your top recommendations into recommendation cards in the NEXT column. You NEVER create running todos \u2014 you only recommend templates by reference. Two tools, strict roles:
43889
+
43890
+ 1. FIND OR CREATE A TEMPLATE (todoregistry-cli):
43891
+ - \`todoregistry-cli search <query>\` / \`todoregistry-cli list -c <category>\` / \`todoregistry-cli get <id>\` \u2014 prefer a matching existing template (proven playbook).
43892
+ - If none fits, CREATE one right there (auth is inherited from your shell):
43893
+ \`todoregistry-cli create --name "<name>" --description "<one line>" --body @<prompt-file>\` (or \`--body -\` for stdin). It prints the new template id on stdout.
43894
+
43895
+ 2. RECOMMEND (todoforai-cli \u2014 references a template id onto the project; never creates a running todo):
43896
+ \`todoforai-cli recommend --template <id> --project <projectId> --business-context <businessContextId> --note "<why>" --priority high|medium|low\`
43897
+ Take <projectId> and <businessContextId> from your current session context and pass them explicitly. The card appears PAUSED in the NEXT column for the user to review before any credits are spent.
43898
+
43899
+ 3. Recommend 3-6 templates matching your Growth Plan (highest priority first), then reply with a one-line summary per card and tell the user they're waiting in the NEXT column for review.`,
43847
43900
  mcpConfigs: {},
43848
43901
  edgesMcpConfigs: {},
43849
43902
  permissions: {
43850
- allow: ["builtin:webfetch", "builtin:google_search", "cloud:google_rag", "builtin:business_context", "builtin:todoforai_api", "builtin:question"],
43903
+ allow: [
43904
+ "builtin:webfetch",
43905
+ "builtin:google_search",
43906
+ "cloud:google_rag",
43907
+ "builtin:todoforai_api",
43908
+ "builtin:question",
43909
+ "cloud:BASH(cmd: todoregistry-cli *)",
43910
+ "cloud:BASH(cmd: todoforai-cli *)",
43911
+ "cloud:BASH(cmd: curl *)"
43912
+ ],
43851
43913
  ask: [],
43852
43914
  deny: []
43853
43915
  },
@@ -43914,6 +43976,10 @@ var TOPICS = {
43914
43976
  channel: (p) => SERVER_TO_FRONTENDS.hexGrid.updated(p.projectId),
43915
43977
  audience: "frontend"
43916
43978
  },
43979
+ ["recommendation:updated" /* RECOMMENDATION_UPDATED */]: {
43980
+ channel: (p) => SERVER_TO_FRONTENDS.recommendation.updated(p.projectId),
43981
+ audience: "frontend"
43982
+ },
43917
43983
  ["sandbox:state" /* SANDBOX_STATE */]: {
43918
43984
  channel: (p) => SERVER_TO_FRONTENDS.sandbox.state(p.userId),
43919
43985
  audience: "frontend"
@@ -44032,19 +44098,30 @@ Usage:
44032
44098
  todoforai-cli --inspect <todo-id>[@<slice>] # Read chat log. <slice> = -3:, :1, 5:10, 7 (Python-style)
44033
44099
  todoforai-cli --template <id> [--input k=v] # Start from a registry template
44034
44100
  todoforai-cli --list-agents # List available agents and exit
44101
+ todoforai-cli --list-models [filter] # List models usable with --model and exit
44035
44102
  todoforai-cli agent update <agent> model=<model> # Update agent settings (see 'agent --help')
44036
44103
  todoforai-cli list [-n 30] [--cursor N] [--all] [--status S] # List todos (paginated); see 'list --help'
44037
44104
  todoforai-cli status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
44038
44105
  todoforai-cli delete <todo-id> # Permanently delete a todo
44039
44106
  todoforai-cli addmessage <todo-id> "text" # Add a message to an existing todo
44107
+ todoforai-cli recommend --template <id> # Add a template as a NEXT-column card (see 'todoregistry-cli create')
44108
+ todoforai-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> ownership links for a project you own
44109
+ todoforai-cli steering get # Show recommendation steering (direction + boosted/muted categories)
44110
+ todoforai-cli steering direction "<text>" # Set the direction the generator should follow
44111
+ todoforai-cli steering boost|mute <cat> # Favour / avoid a category
44112
+ todoforai-cli next # Generate steered NEXT-column recommendations
44040
44113
 
44041
44114
  Options:
44042
44115
  --path <dir> Workspace path (default: cwd)
44043
44116
  --project <id> Project ID
44044
44117
  --agent, -a <name> Agent name (partial match)
44118
+ --model <model> Override the agent's model for this todo
44119
+ (e.g. anthropic:anthropic/claude-opus-5, openai:openai/gpt-5.6-sol)
44045
44120
  --list-agents List available agents (name, id, workspace paths) and exit
44121
+ --list-models List models usable with --model (optional substring filter) and exit
44046
44122
  --api-url <url> API URL
44047
44123
  --api-key <key> API key
44124
+ --user-id <id> Admin HTTP impersonation; requires --no-watch
44048
44125
  --inspect, -i <todo-id>[@<slice>] Print chat log (read-only)
44049
44126
  --template, -t <id> Start from a registry template
44050
44127
  --input <key=value> Template input (repeatable)
@@ -44098,11 +44175,20 @@ function parseCliArgs() {
44098
44175
  agent: { type: "string", short: "a" },
44099
44176
  model: { type: "string" },
44100
44177
  "list-agents": { type: "boolean", default: false },
44178
+ "list-models": { type: "boolean", default: false },
44101
44179
  "api-url": { type: "string" },
44102
44180
  "api-key": { type: "string" },
44181
+ "user-id": { type: "string" },
44103
44182
  inspect: { type: "string", short: "i" },
44104
44183
  template: { type: "string", short: "t" },
44105
44184
  input: { type: "string", multiple: true },
44185
+ note: { type: "string" },
44186
+ priority: { type: "string" },
44187
+ title: { type: "string" },
44188
+ "business-context": { type: "string" },
44189
+ seed: { type: "string" },
44190
+ emails: { type: "string" },
44191
+ ttl: { type: "string" },
44106
44192
  resume: { type: "string", short: "r" },
44107
44193
  continue: { type: "boolean", short: "c", default: false },
44108
44194
  "non-interactive": { type: "boolean", short: "n", default: false },
@@ -44550,17 +44636,22 @@ function ensureBridgeCredentials(apiUrl) {
44550
44636
  }
44551
44637
  function bridgeAlreadyRunning() {
44552
44638
  if (process.platform === "win32")
44553
- return false;
44554
- const r = spawnSync("pgrep", ["-f", "todoforai-bridge"], { encoding: "utf-8" });
44555
- return r.status === 0 && (r.stdout || "").trim().length > 0;
44639
+ return Promise.resolve(false);
44640
+ return new Promise((resolve) => {
44641
+ const r = spawn("pgrep", ["-f", "todoforai-bridge"]);
44642
+ let out = "";
44643
+ r.stdout.on("data", (d) => out += d);
44644
+ r.on("error", () => resolve(false));
44645
+ r.on("close", (code) => resolve(code === 0 && out.trim().length > 0));
44646
+ });
44556
44647
  }
44557
- function ensureBridgeRunning(apiUrl, _apiKey) {
44648
+ async function ensureBridgeRunning(apiUrl, _apiKey) {
44649
+ if (await bridgeAlreadyRunning())
44650
+ return;
44558
44651
  if (!hasBridge()) {
44559
44652
  console.error("\x1B[2mBridge not started: `todoforai-bridge` was not found on PATH. Install TODOforAI Bridge, or pass --no-bridge (or deprecated --no-edge) to silence this.\x1B[0m");
44560
44653
  return;
44561
44654
  }
44562
- if (bridgeAlreadyRunning())
44563
- return;
44564
44655
  if (!ensureBridgeCredentials(apiUrl)) {
44565
44656
  console.error("\x1B[33mBridge not started: `todoforai-bridge login` did not complete successfully.\x1B[0m");
44566
44657
  return;
@@ -44784,40 +44875,38 @@ class ScopedConfig {
44784
44875
  }
44785
44876
  }
44786
44877
 
44787
- // src/credentials.ts
44788
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
44789
- import { dirname as dirname2, join as join2 } from "path";
44878
+ // ../packages/shared-credentials/src/index.ts
44879
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
44880
+ import { join as join2, dirname as dirname2 } from "path";
44790
44881
  import { homedir as homedir2, platform as platform2 } from "os";
44791
44882
  function credentialsPath2() {
44792
- const sys = platform2();
44793
- if (sys === "win32")
44794
- return join2(homedir2(), "AppData", "Roaming", "todoforai", "credentials.json");
44795
- if (sys === "darwin")
44796
- return join2(homedir2(), "Library", "Application Support", "todoforai", "credentials.json");
44797
- const xdg = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
44798
- return join2(xdg, "todoforai", "credentials.json");
44883
+ return platform2() === "win32" ? join2(homedir2(), "AppData", "Roaming", "todoforai", "credentials.json") : platform2() === "darwin" ? join2(homedir2(), "Library", "Application Support", "todoforai", "credentials.json") : join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "todoforai", "credentials.json");
44799
44884
  }
44800
- var CREDENTIALS_PATH2 = credentialsPath2();
44801
44885
  function read() {
44802
- if (!existsSync2(CREDENTIALS_PATH2))
44803
- return {};
44804
44886
  try {
44805
- return JSON.parse(readFileSync2(CREDENTIALS_PATH2, "utf-8"));
44887
+ return JSON.parse(readFileSync2(credentialsPath2(), "utf-8"));
44806
44888
  } catch {
44807
44889
  return {};
44808
44890
  }
44809
44891
  }
44892
+ function writeSecure(data) {
44893
+ const p = credentialsPath2();
44894
+ mkdirSync2(dirname2(p), { recursive: true });
44895
+ writeFileSync2(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
44896
+ try {
44897
+ chmodSync(p, 384);
44898
+ } catch {}
44899
+ }
44810
44900
  function readCredential(apiUrl) {
44811
- const v = read()[apiUrl];
44901
+ const c2 = read();
44902
+ const v = c2[apiUrl] || c2.apiToken || c2.apiKey;
44812
44903
  return typeof v === "string" ? v : "";
44813
44904
  }
44814
44905
  function writeCredential(apiUrl, apiKey) {
44815
- const data = read();
44816
- data[apiUrl] = apiKey;
44817
- mkdirSync2(dirname2(CREDENTIALS_PATH2), { recursive: true });
44818
- writeFileSync2(CREDENTIALS_PATH2, JSON.stringify(data, null, 2), "utf-8");
44906
+ const c2 = read();
44907
+ c2[apiUrl] = apiKey;
44908
+ writeSecure(c2);
44819
44909
  }
44820
-
44821
44910
  // src/logo.ts
44822
44911
  var LETTERS = {
44823
44912
  t: [" x ", "xxxx", " x ", " xll", " xll", " xxx"],
@@ -44828,11 +44917,10 @@ var LETTERS = {
44828
44917
  c: [" ", "xxxx", "x ", "xlll", "xlll", "xxxx"],
44829
44918
  e: [" ", "xxxx", "x x", "xxxx", "xlll", "xxxx"],
44830
44919
  a: [" ", "xxxx", " x", "xxxx", "xllx", "xxxx"],
44831
- i: ["x", " ", "x", "x", "x", "x"],
44832
- "4": [" ", " x ", " xx ", "xlxl", "xxxx", "llxl"]
44920
+ i: ["x", " ", "x", "x", "x", "x"]
44833
44921
  };
44834
44922
  var GAP = " ";
44835
- var WORD = "todo4ai";
44923
+ var WORD = "todoforai";
44836
44924
  function renderHalfBlock(top, bot) {
44837
44925
  const W = c("\x1B[38;2;249;110;46m");
44838
44926
  const G = c("\x1B[38;2;140;60;20m");
@@ -45993,6 +46081,20 @@ async function listAgentsCommand(api, opts) {
45993
46081
  }
45994
46082
  }
45995
46083
 
46084
+ // src/list-models.ts
46085
+ async function listModelsCommand(api, opts) {
46086
+ const res = await api.listModels();
46087
+ const q = opts.filter?.toLowerCase();
46088
+ const ids = (res?.data || []).map((m) => String(m.id).includes("/") ? `${String(m.id).split("/")[0]}:${m.id}` : String(m.id)).filter((id) => !q || id.toLowerCase().includes(q));
46089
+ if (opts.json) {
46090
+ console.log(JSON.stringify(ids, null, 2));
46091
+ return;
46092
+ }
46093
+ for (const id of ids)
46094
+ process.stderr.write(`${BRAND}${id}${RESET}
46095
+ `);
46096
+ }
46097
+
45996
46098
  // src/agent-command.ts
45997
46099
  function printAgentHelp() {
45998
46100
  process.stderr.write(`
@@ -46302,6 +46404,92 @@ async function listTodosCommand(api, defaultProjectId, argv) {
46302
46404
  `);
46303
46405
  }
46304
46406
 
46407
+ // src/steering-command.ts
46408
+ var RED2 = "\x1B[31m";
46409
+ var GREEN2 = "\x1B[32m";
46410
+ var DIM2 = "\x1B[2m";
46411
+ var RESET2 = "\x1B[0m";
46412
+ async function rest(ctx, method, path4, body) {
46413
+ const res = await fetch(`${ctx.apiUrl}/api/v1${path4}`, {
46414
+ method,
46415
+ headers: { "content-type": "application/json", "x-api-key": ctx.apiKey },
46416
+ ...body ? { body: JSON.stringify(body) } : {},
46417
+ signal: AbortSignal.timeout(30000)
46418
+ });
46419
+ const text = await res.text();
46420
+ if (!res.ok)
46421
+ throw new Error(`${method} ${path4} failed: ${res.status} ${text}`);
46422
+ return text ? JSON.parse(text) : null;
46423
+ }
46424
+ function printSteering(s) {
46425
+ process.stderr.write(`${GREEN2}Direction:${RESET2} ${s.direction || DIM2 + "(none)" + RESET2}
46426
+ `);
46427
+ process.stderr.write(`${GREEN2}Boosted:${RESET2} ${s.boostedCategoryIds?.join(", ") || DIM2 + "(none)" + RESET2}
46428
+ `);
46429
+ process.stderr.write(`${GREEN2}Muted:${RESET2} ${s.mutedCategoryIds?.join(", ") || DIM2 + "(none)" + RESET2}
46430
+ `);
46431
+ }
46432
+ var USAGE = `${RED2}Usage:
46433
+ todoforai-cli steering get [--project <id>]
46434
+ todoforai-cli steering direction "<text>" [--project <id>] (empty text clears it)
46435
+ todoforai-cli steering boost <categoryId> [--project <id>]
46436
+ todoforai-cli steering mute <categoryId> [--project <id>]
46437
+ todoforai-cli steering clear [--project <id>] (clears all steering)${RESET2}`;
46438
+ async function steeringCommand(ctx, argv) {
46439
+ const sub = argv[0];
46440
+ const path4 = `/projects/${ctx.projectId}/recommendation-steering`;
46441
+ if (!sub || sub === "get") {
46442
+ const s = await rest(ctx, "GET", path4);
46443
+ if (ctx.json)
46444
+ console.log(JSON.stringify(s, null, 2));
46445
+ else
46446
+ printSteering(s);
46447
+ return;
46448
+ }
46449
+ if (sub === "direction") {
46450
+ const text = argv.slice(1).join(" ").trim();
46451
+ const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, direction: text || null });
46452
+ if (ctx.json)
46453
+ console.log(JSON.stringify(s, null, 2));
46454
+ else
46455
+ process.stderr.write(`${GREEN2}\u2705 ${text ? `Direction set: ${text}` : "Direction cleared"}${RESET2}
46456
+ `);
46457
+ return;
46458
+ }
46459
+ if (sub === "boost" || sub === "mute") {
46460
+ const categoryId = argv[1];
46461
+ if (!categoryId) {
46462
+ process.stderr.write(USAGE + `
46463
+ `);
46464
+ process.exit(2);
46465
+ }
46466
+ const current = await rest(ctx, "GET", path4);
46467
+ const key = sub === "boost" ? "boostedCategoryIds" : "mutedCategoryIds";
46468
+ const other = sub === "boost" ? "mutedCategoryIds" : "boostedCategoryIds";
46469
+ const next = Array.from(new Set([...current[key] ?? [], categoryId]));
46470
+ const nextOther = (current[other] ?? []).filter((c2) => c2 !== categoryId);
46471
+ const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, [key]: next, [other]: nextOther });
46472
+ if (ctx.json)
46473
+ console.log(JSON.stringify(s, null, 2));
46474
+ else
46475
+ process.stderr.write(`${GREEN2}\u2705 ${sub === "boost" ? "Boosted" : "Muted"} ${categoryId}${RESET2}
46476
+ `);
46477
+ return;
46478
+ }
46479
+ if (sub === "clear") {
46480
+ const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, direction: null, boostedCategoryIds: [], mutedCategoryIds: [] });
46481
+ if (ctx.json)
46482
+ console.log(JSON.stringify(s, null, 2));
46483
+ else
46484
+ process.stderr.write(`${GREEN2}\u2705 Steering cleared${RESET2}
46485
+ `);
46486
+ return;
46487
+ }
46488
+ process.stderr.write(USAGE + `
46489
+ `);
46490
+ process.exit(2);
46491
+ }
46492
+
46305
46493
  // src/index.ts
46306
46494
  try {
46307
46495
  const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
@@ -46419,8 +46607,8 @@ Cancelled by user (Ctrl+C)
46419
46607
  return;
46420
46608
  }
46421
46609
  if (args["reset-config"]) {
46422
- const { existsSync: existsSync3, unlinkSync } = await import("fs");
46423
- if (existsSync3(cfg.path)) {
46610
+ const { existsSync: existsSync2, unlinkSync } = await import("fs");
46611
+ if (existsSync2(cfg.path)) {
46424
46612
  unlinkSync(cfg.path);
46425
46613
  console.log(`Configuration reset: ${formatPathWithTilde(cfg.path)}`);
46426
46614
  } else
@@ -46429,6 +46617,15 @@ Cancelled by user (Ctrl+C)
46429
46617
  }
46430
46618
  const apiUrl = normalizeApiUrl(args["api-url"] || getEnv("API_URL") || DEFAULT_API_URL);
46431
46619
  const cfgScope = cfg.scope(apiUrl);
46620
+ if (args["user-id"]) {
46621
+ const url = new URL(apiUrl);
46622
+ const loopback = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname);
46623
+ if (url.protocol !== "http:" || !loopback) {
46624
+ process.stderr.write(`Error: --user-id is restricted to HTTP loopback URLs
46625
+ `);
46626
+ process.exit(2);
46627
+ }
46628
+ }
46432
46629
  async function deviceLogin() {
46433
46630
  const loginApi = new ApiClient(apiUrl, "");
46434
46631
  const { code, url, expiresIn } = await loginApi.initDeviceLogin("edge");
@@ -46484,11 +46681,27 @@ Cancelled by user (Ctrl+C)
46484
46681
  await deviceLogin();
46485
46682
  return;
46486
46683
  }
46487
- let apiKey = args["api-key"] || readCredential(apiUrl) || getEnv("API_TOKEN") || "";
46684
+ const notDst = (t) => t.startsWith("dst_") ? "" : t;
46685
+ let apiKey = args["api-key"] || notDst(readCredential(apiUrl)) || notDst(getEnv("API_TOKEN")) || "";
46488
46686
  if (!apiKey) {
46489
46687
  apiKey = await deviceLogin();
46490
46688
  }
46491
- const api = new ApiClient(apiUrl, apiKey);
46689
+ const api = new ApiClient(apiUrl, apiKey, args["user-id"]);
46690
+ if (args["user-id"] && args.safe) {
46691
+ process.stderr.write(`Error: --safe is not supported with --user-id
46692
+ `);
46693
+ process.exit(2);
46694
+ }
46695
+ if (args["user-id"] && (args.resume || args.continue)) {
46696
+ process.stderr.write(`Error: resume is not supported with --user-id
46697
+ `);
46698
+ process.exit(2);
46699
+ }
46700
+ if (args["user-id"] && args.template && !args["no-watch"]) {
46701
+ process.stderr.write(`Error: --user-id uses admin HTTP impersonation and requires --no-watch
46702
+ `);
46703
+ process.exit(2);
46704
+ }
46492
46705
  if (positionals[0] === "status") {
46493
46706
  const [, todoId, status] = positionals;
46494
46707
  if (!todoId || !status) {
@@ -46513,8 +46726,8 @@ Cancelled by user (Ctrl+C)
46513
46726
  return;
46514
46727
  }
46515
46728
  if (positionals[0] === "addmessage") {
46516
- const [, todoId, ...rest] = positionals;
46517
- const content2 = rest.join(" ") || await readStdin();
46729
+ const [, todoId, ...rest2] = positionals;
46730
+ const content2 = rest2.join(" ") || await readStdin();
46518
46731
  if (!todoId || !content2) {
46519
46732
  process.stderr.write(`${RED}Usage: todoforai-cli addmessage <todo-id> "content"${RESET}
46520
46733
  `);
@@ -46529,10 +46742,132 @@ Cancelled by user (Ctrl+C)
46529
46742
  `);
46530
46743
  return;
46531
46744
  }
46745
+ if (positionals[0] === "recommend") {
46746
+ const templateId = args.template || positionals[1];
46747
+ if (!templateId) {
46748
+ process.stderr.write(`${RED}Usage: todoforai-cli recommend --template <id> [--note "why"] [--priority high|medium|low] [--title "..."] [--project <id>]${RESET}
46749
+ `);
46750
+ process.stderr.write(`${DIM}Create a template first with: todoregistry-cli create --name ... --description ... --body @prompt.md${RESET}
46751
+ `);
46752
+ process.exit(2);
46753
+ }
46754
+ let projectId2 = args.project || cfgScope.data.default_project_id;
46755
+ if (!projectId2) {
46756
+ const projects2 = await api.listProjects();
46757
+ projectId2 = projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
46758
+ }
46759
+ if (!projectId2) {
46760
+ process.stderr.write(`${RED}No project found \u2014 pass --project <id>${RESET}
46761
+ `);
46762
+ process.exit(2);
46763
+ }
46764
+ const priority = args.priority;
46765
+ if (priority && !["high", "medium", "low"].includes(priority)) {
46766
+ process.stderr.write(`${RED}--priority must be high, medium, or low${RESET}
46767
+ `);
46768
+ process.exit(2);
46769
+ }
46770
+ const rec = await api.recommend({
46771
+ projectId: projectId2,
46772
+ templateId,
46773
+ ...args.title ? { title: args.title } : {},
46774
+ ...args.note ? { note: args.note } : {},
46775
+ ...priority ? { priority } : {},
46776
+ ...args["business-context"] ? { businessContextId: args["business-context"] } : {}
46777
+ });
46778
+ if (args.json)
46779
+ console.log(JSON.stringify(rec, null, 2));
46780
+ else
46781
+ process.stderr.write(`${GREEN}\u2705 Recommended template ${templateId} on project ${projectId2}${RESET}
46782
+ `);
46783
+ return;
46784
+ }
46785
+ if (positionals[0] === "claim" && positionals[1] === "mint") {
46786
+ let seedProjectId = args.seed || args.project || cfgScope.data.default_project_id;
46787
+ if (!seedProjectId) {
46788
+ const projects2 = await api.listProjects();
46789
+ seedProjectId = projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
46790
+ }
46791
+ if (!seedProjectId) {
46792
+ process.stderr.write(`${RED}Usage: todoforai-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>]${RESET}
46793
+ `);
46794
+ process.exit(2);
46795
+ }
46796
+ const emails = args.emails?.split(",").map((e) => e.trim()).filter(Boolean);
46797
+ let ttlSec;
46798
+ if (args.ttl !== undefined) {
46799
+ ttlSec = Number(args.ttl);
46800
+ if (!Number.isFinite(ttlSec)) {
46801
+ process.stderr.write(`${RED}--ttl must be a number (seconds)${RESET}
46802
+ `);
46803
+ process.exit(2);
46804
+ }
46805
+ }
46806
+ const res = await fetch(`${apiUrl}/api/v1/claims/mint`, {
46807
+ method: "POST",
46808
+ headers: { "content-type": "application/json", "x-api-key": apiKey },
46809
+ body: JSON.stringify({ seedProjectId, ...emails?.length ? { emails } : {}, ...ttlSec !== undefined ? { ttlSec } : {} }),
46810
+ signal: AbortSignal.timeout(30000)
46811
+ });
46812
+ const text = await res.text();
46813
+ if (!res.ok) {
46814
+ process.stderr.write(`${RED}Mint failed: ${res.status} ${text}${RESET}
46815
+ `);
46816
+ process.exit(1);
46817
+ }
46818
+ const out = JSON.parse(text);
46819
+ if (args.json)
46820
+ console.log(JSON.stringify(out, null, 2));
46821
+ else
46822
+ for (const l of out.links)
46823
+ process.stdout.write(`${l.email ? `${l.email} ` : ""}${CYAN}${l.url}${RESET}
46824
+ `);
46825
+ return;
46826
+ }
46827
+ if (positionals[0] === "steering" || positionals[0] === "next") {
46828
+ let projectId2 = args.project || cfgScope.data.default_project_id;
46829
+ if (!projectId2) {
46830
+ const projects2 = await api.listProjects();
46831
+ projectId2 = projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
46832
+ }
46833
+ if (!projectId2) {
46834
+ process.stderr.write(`${RED}No project found \u2014 pass --project <id>${RESET}
46835
+ `);
46836
+ process.exit(2);
46837
+ }
46838
+ const sctx = { apiUrl, apiKey, projectId: projectId2, json: !!args.json };
46839
+ if (positionals[0] === "next") {
46840
+ const res = await fetch(`${apiUrl}/api/v1/projects/${projectId2}/recommendations/generate`, {
46841
+ method: "POST",
46842
+ headers: { "content-type": "application/json", "x-api-key": apiKey },
46843
+ body: JSON.stringify({ projectId: projectId2, ...args["business-context"] ? { businessContextId: args["business-context"] } : {} }),
46844
+ signal: AbortSignal.timeout(30000)
46845
+ });
46846
+ const text = await res.text();
46847
+ if (!res.ok) {
46848
+ process.stderr.write(`${RED}Generate failed: ${res.status} ${text}${RESET}
46849
+ `);
46850
+ process.exit(1);
46851
+ }
46852
+ const out = JSON.parse(text);
46853
+ if (args.json)
46854
+ console.log(JSON.stringify(out, null, 2));
46855
+ else
46856
+ process.stderr.write(`${GREEN}\u2705 Generating recommendations \u2014 analyzer todo ${out.todoId}${RESET}
46857
+ `);
46858
+ return;
46859
+ }
46860
+ await steeringCommand(sctx, positionals.slice(1));
46861
+ return;
46862
+ }
46532
46863
  if (args["list-agents"]) {
46533
46864
  await listAgentsCommand(api, { json: !!args.json, formatPath: formatPathWithTilde });
46534
46865
  return;
46535
46866
  }
46867
+ if (args["list-models"]) {
46868
+ await listModelsCommand(api, { json: !!args.json, filter: positionals[0] });
46869
+ return;
46870
+ }
46536
46871
  if (positionals[0] === "agent") {
46537
46872
  await agentCommand(api, positionals, args, formatPathWithTilde);
46538
46873
  return;
@@ -46632,6 +46967,8 @@ Cancelled by user (Ctrl+C)
46632
46967
  const frontendUrl2 = getFrontendUrl(apiUrl, projectId2, todoId);
46633
46968
  if (args.json) {
46634
46969
  console.log(JSON.stringify({ ...todo2, frontend_url: frontendUrl2 }, null, 2));
46970
+ } else if (args["no-watch"]) {
46971
+ console.log(`created ${todoId}`);
46635
46972
  } else {
46636
46973
  process.stderr.write(`${DIM}TODO:${RESET} ${CYAN}${frontendUrl2}${RESET}
46637
46974
  `);
@@ -46706,6 +47043,15 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46706
47043
  await ws2.close();
46707
47044
  return;
46708
47045
  }
47046
+ if (args["user-id"] && !args["no-watch"]) {
47047
+ process.stderr.write(`Error: --user-id uses admin HTTP impersonation and requires --no-watch
47048
+ `);
47049
+ process.exit(2);
47050
+ }
47051
+ const ws = args["no-watch"] ? null : new FrontendWebSocket(apiUrl, apiKey);
47052
+ const wsReady = ws ? ws.connect() : null;
47053
+ if (!args["no-bridge"] && !args["no-watch"])
47054
+ ensureBridgeRunning(apiUrl, apiKey);
46709
47055
  let preMatchedAgent = null;
46710
47056
  let agents = null;
46711
47057
  if (args.agent) {
@@ -46757,8 +47103,6 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46757
47103
  }
46758
47104
  process.stderr.write(`${DIM}Tip: ${randomTip()}${RESET}
46759
47105
  `);
46760
- if (!args["no-bridge"] && !args["no-watch"])
46761
- ensureBridgeRunning(apiUrl, apiKey);
46762
47106
  let content;
46763
47107
  if (positionals.length > 0) {
46764
47108
  content = positionals.join(" ");
@@ -46800,9 +47144,8 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46800
47144
  } else {
46801
47145
  agent = await selectAgent(agents, cfgScope.data.default_agent_name, (name, settings) => cfgScope.setDefaultAgent(name, settings));
46802
47146
  }
46803
- const ws = args["no-watch"] ? null : new FrontendWebSocket(apiUrl, apiKey);
46804
- if (ws)
46805
- await ws.connect();
47147
+ if (wsReady)
47148
+ await wsReady;
46806
47149
  if (args.model)
46807
47150
  agent = { ...agent, model: args.model };
46808
47151
  if (args["raw-sysmsg"]) {
@@ -46820,6 +47163,8 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46820
47163
  const frontendUrl = getFrontendUrl(apiUrl, projectId, actualTodoId);
46821
47164
  if (args.json) {
46822
47165
  console.log(JSON.stringify({ ...todo, frontend_url: frontendUrl }, null, 2));
47166
+ } else if (!ws) {
47167
+ console.log(`created ${actualTodoId}`);
46823
47168
  } else {
46824
47169
  process.stderr.write(`${DIM}TODO:${RESET} ${CYAN}${frontendUrl}${RESET}
46825
47170
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "dist/todoai.js",
@@ -16,6 +16,7 @@
16
16
  "dev": "bun run src/index.ts"
17
17
  },
18
18
  "dependencies": {
19
+ "@shared/credentials": "file:../packages/shared-credentials",
19
20
  "@todoforai/update-notifier": "^0.1.0",
20
21
  "cli-highlight": "^2.1.11",
21
22
  "diff-match-patch": "^1.0.5",