@todoforai/cli 0.1.22 → 0.1.25

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.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ // Dev (linked repo): run src directly so there is no stale-build step.
3
+ // Published package ships only dist/ (the @shared/* file: deps are bundled in).
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const src = fileURLToPath(new URL("../src/index.ts", import.meta.url));
8
+ const dist = fileURLToPath(new URL("../dist/todoai.js", import.meta.url));
9
+ await import(existsSync(src) ? src : dist);
package/dist/todoai.js CHANGED
@@ -42732,6 +42732,10 @@ function randomTip() {
42732
42732
  }
42733
42733
 
42734
42734
  // ../edge/bun/src/api.ts
42735
+ function restBasePath(apiKey) {
42736
+ return apiKey.startsWith("dst_") ? "/dst/v1" : "/api/v1";
42737
+ }
42738
+
42735
42739
  class ApiClient {
42736
42740
  apiUrl;
42737
42741
  apiKey;
@@ -42743,7 +42747,7 @@ class ApiClient {
42743
42747
  return { "content-type": "application/json", "x-api-key": this.apiKey };
42744
42748
  }
42745
42749
  async request(method, endpoint, body) {
42746
- const url = `${this.apiUrl}${endpoint}`;
42750
+ const url = `${this.apiUrl}${endpoint.replace(/^\/api\/v1/, restBasePath(this.apiKey))}`;
42747
42751
  const opts = { method, headers: this.headers, signal: AbortSignal.timeout(30000) };
42748
42752
  if (body)
42749
42753
  opts.body = JSON.stringify(body);
@@ -42777,7 +42781,7 @@ class ApiClient {
42777
42781
  if (!this.apiKey)
42778
42782
  return { valid: false, error: "No API key provided" };
42779
42783
  try {
42780
- const res = await fetch(`${this.apiUrl}/api/v1/apikey/validate`, {
42784
+ const res = await fetch(`${this.apiUrl}${restBasePath(this.apiKey)}/apikey/validate`, {
42781
42785
  headers: { "x-api-key": this.apiKey },
42782
42786
  signal: AbortSignal.timeout(1e4)
42783
42787
  });
@@ -42814,6 +42818,13 @@ class ApiClient {
42814
42818
  listTodos(projectId, opts) {
42815
42819
  if (!projectId)
42816
42820
  return this.request("GET", "/api/v1/todos");
42821
+ if (this.apiKey.startsWith("dst_")) {
42822
+ const qs = new URLSearchParams;
42823
+ for (const [k, v] of Object.entries(opts ?? {}))
42824
+ if (v !== undefined)
42825
+ qs.set(k, String(v));
42826
+ return this.request("GET", `/api/v1/projects/${projectId}/todos${qs.size ? `?${qs}` : ""}`);
42827
+ }
42817
42828
  return this.trpcQuery("todo.list", { projectId, ...opts });
42818
42829
  }
42819
42830
  getTodo(todoId) {
@@ -42875,17 +42886,21 @@ class ApiClient {
42875
42886
  listDevices() {
42876
42887
  return this.request("GET", "/api/v1/devices");
42877
42888
  }
42878
- startFromTemplate(projectId, templateId, opts) {
42889
+ startFromSpec(projectId, specId, opts) {
42879
42890
  return this.request("POST", `/api/v1/projects/${projectId}/todos/from-template`, {
42880
- templateId,
42891
+ specId,
42881
42892
  ...opts
42882
42893
  });
42883
42894
  }
42884
- async getRegistryTemplate(templateId) {
42895
+ recommend(input) {
42896
+ const { projectId, ...body } = input;
42897
+ return this.request("POST", `/api/v1/projects/${projectId}/recommendations`, body);
42898
+ }
42899
+ async getRegistrySpec(specId) {
42885
42900
  const base = this.apiUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
42886
- const res = await fetch(`${base}/cookie/v1/registry/templates/${templateId}`);
42901
+ const res = await fetch(`${base}/cookie/v1/registry/templates/${specId}`);
42887
42902
  if (!res.ok)
42888
- throw new Error(`Template '${templateId}' not found (${res.status})`);
42903
+ throw new Error(`Spec '${specId}' not found (${res.status})`);
42889
42904
  return res.json();
42890
42905
  }
42891
42906
  async initDeviceLogin(clientName = "edge") {
@@ -42904,7 +42919,7 @@ class ApiClient {
42904
42919
  throw new Error(`Device login poll failed: ${res.status}`);
42905
42920
  return res.json();
42906
42921
  }
42907
- async listRegistryTemplates(category = "all") {
42922
+ async listRegistrySpecs(category = "all") {
42908
42923
  const base = this.apiUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
42909
42924
  const res = await fetch(`${base}/cookie/v1/registry/templates?category=${category}`);
42910
42925
  if (!res.ok)
@@ -43135,13 +43150,16 @@ import { parseArgs } from "util";
43135
43150
  // package.json
43136
43151
  var package_default = {
43137
43152
  name: "@todoforai/cli",
43138
- version: "0.1.3",
43153
+ version: "0.1.24",
43139
43154
  type: "module",
43140
43155
  bin: {
43141
- "todoforai-cli": "dist/todoai.js",
43142
- todoai: "dist/todoai.js"
43156
+ "todoforai-cli": "bin/todoforai-cli.js",
43157
+ todoai: "bin/todoforai-cli.js"
43143
43158
  },
43144
- files: ["dist/todoai.js"],
43159
+ files: [
43160
+ "bin",
43161
+ "dist/todoai.js"
43162
+ ],
43145
43163
  scripts: {
43146
43164
  build: "bun build src/index.ts --target=bun --outfile dist/todoai.js --external ws",
43147
43165
  prepublishOnly: "bun run build",
@@ -43400,8 +43418,8 @@ var SERVER_TO_FRONTENDS = {
43400
43418
  hexGrid: {
43401
43419
  updated: (projectId) => `${"hexgrid:updated" /* HEXGRID_UPDATED */}:${projectId}`
43402
43420
  },
43403
- recommendation: {
43404
- updated: (projectId) => `${"recommendation:updated" /* RECOMMENDATION_UPDATED */}:${projectId}`
43421
+ placement: {
43422
+ updated: (projectId) => `${"placement:updated" /* PLACEMENT_UPDATED */}:${projectId}`
43405
43423
  },
43406
43424
  sandbox: {
43407
43425
  state: (userId) => `${"sandbox:state" /* SANDBOX_STATE */}:${userId}`
@@ -43447,6 +43465,7 @@ var SERVER_TO_FRONTENDS = {
43447
43465
  block_update: (todoId) => `todo:${todoId}:block_update`,
43448
43466
  context_summary_updated: (todoId) => `${"context:summary_updated" /* CONTEXT_SUMMARY_UPDATED */}:${todoId}`,
43449
43467
  context_info_updated: (todoId) => `${"context:info_updated" /* CONTEXT_INFO_UPDATED */}:${todoId}`,
43468
+ predicted_reply_updated: (todoId) => `${"predict:reply_updated" /* PREDICTED_REPLY_UPDATED */}:${todoId}`,
43450
43469
  error_result: (todoId) => `${"block:error_result" /* BLOCK_ERROR_RESULT */}:${todoId}`,
43451
43470
  meta_result: (todoId) => `${"block:meta_result" /* BLOCK_META_RESULT */}:${todoId}`,
43452
43471
  file_changed: (edgeId, path3) => `${"block:file_changed" /* BLOCK_FILE_CHANGED */}:${edgeId}:${path3}`,
@@ -43827,12 +43846,12 @@ var BUSINESS_ANALYZER_AGENT = {
43827
43846
 
43828
43847
  You have no previous context about this business yet \u2014 everything you know must come from the current business context markdown and what you research now. Do not assume prior knowledge or earlier conversations.
43829
43848
 
43830
- This chat lives on the user's Brand page: the business context markdown you maintain is rendered live right next to this conversation, together with the brand voice answers and the company's online-presence map. Whenever you call update_business_context, the user sees the document change immediately \u2014 refer to it as "your brand page" (never as a file), and keep it polished since it doubles as the company's profile.
43849
+ This chat lives on the user's Brand page: the business context markdown you maintain is rendered live right next to this conversation, together with the brand voice answers and the company's online-presence map. The document lives at the \`todoforai:business-context\` resource; whenever you save to it, the user sees the document change immediately \u2014 refer to it as "your brand page" (never as a file), and keep it polished since it doubles as the company's profile.
43831
43850
 
43832
43851
  How to analyze and update the business context:
43833
- 1. Read the current business context markdown from the system prompt
43852
+ 1. Read the current business context with \`Read todoforai:business-context\`
43834
43853
  2. Research: fetch websites with webfetch (follow key subpages like /about, /pricing, /contact if they exist); use google_search and google_rag to find competitors and fill knowledge gaps
43835
- 3. Merge findings into the existing context \u2014 preserve valid existing information, update conflicting facts only when the new source is clearer or more specific \u2014 and save with update_business_context
43854
+ 3. Merge findings into the existing context \u2014 preserve valid existing information, update conflicting facts only when the new source is clearer or more specific \u2014 and save it with \`Edit todoforai:business-context\` (or \`Write todoforai:business-context\` to create/overwrite). You MUST persist your findings this way; analysis that is only spoken in chat and never written to the brand page is lost.
43836
43855
 
43837
43856
  Cover these sections (skip any without data):
43838
43857
  - **Company Info** \u2014 name, founding year, location, team size, stage
@@ -43846,7 +43865,7 @@ Cover these sections (skip any without data):
43846
43865
 
43847
43866
  If anything important remains unclear, add an **Open Questions** section at the end.
43848
43867
 
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):
43868
+ Maintain a single machine-readable **Presence** block so the user's brand page can render a live presence map. Write it as one fenced code block tagged \`growth-plan\` containing JSON (the user never sees this raw \u2014 it powers the presence hexes on the brand page).
43850
43869
 
43851
43870
  \`\`\`growth-plan
43852
43871
  {
@@ -43854,25 +43873,16 @@ Maintain a single machine-readable **Growth Plan** block so the user's brand pag
43854
43873
  "linkedin-networking": { "score": 45, "handle": "https://linkedin.com/company/example", "note": "Company page exists but posts infrequently" },
43855
43874
  "x-twitter-presence": { "score": 60, "handle": "@example", "note": "Active, regular posts" },
43856
43875
  "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
- ]
43876
+ }
43868
43877
  }
43869
43878
  \`\`\`
43870
43879
 
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.
43880
+ Rules for the Presence block:
43881
+ - Keys 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. Skip channels you did not check.
43873
43882
  - \`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.
43883
+ - Keep it consistent with the prose Online Presence section \u2014 the JSON is the structured mirror of those findings.
43884
+
43885
+ Growth recommendations are NOT part of this block \u2014 they live in the project's Recommendations column as placement cards (see "Recommending actionable TODOs" below). After any analysis that surfaces growth opportunities, materialize your top recommendations as cards there; the prose Next Steps section stays human-readable advice.
43876
43886
 
43877
43887
  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
43888
 
@@ -43882,21 +43892,28 @@ Guide the user one step at a time \u2014 they should always know the single next
43882
43892
 
43883
43893
  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
43894
 
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.
43895
+ When the user asks you to recommend how to grow (e.g. "recommend growth TODOs", the brand page's guided suggestion, the board's Recommend button), read the current business context (including the presence block), research only where information is missing or stale, refresh the presence block if findings changed, then materialize your top growth actions as recommendation cards (see below). The cards ARE the growth plan \u2014 do not write a separate recommendations list into the markdown.
43886
43896
 
43887
43897
  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:
43898
+ To materialize growth recommendations, create recommendation cards in the project's Recommendations column. You NEVER create running todos \u2014 you only recommend templates by reference. Two tools, strict roles:
43889
43899
 
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):
43900
+ 1. GET A TEMPLATE (todoregistry-cli) \u2014 generate-first, reuse only on a close fit:
43901
+ - Default: CREATE a template tailored to THIS business, right there (auth is inherited from your shell):
43893
43902
  \`todoregistry-cli create --name "<name>" --description "<one line>" --body @<prompt-file>\` (or \`--body -\` for stdin). It prints the new template id on stdout.
43903
+ - Before creating, you MAY \`todoregistry-cli search <query>\` / \`list -c <category>\` / \`get <id>\`; reuse an existing template ONLY when it already fits this business closely with no meaningful edits. If it needs tailoring, create a new one instead \u2014 do not recommend a generic playbook that ignores this company's specifics.
43904
+
43905
+ Write every generated body to be unmistakably about THIS company \u2014 bake the specifics from the business context into the task itself: the company name, its actual competitors, the target segment, the specific channel/platform, and the concrete metric to move. A reader should never mistake it for a generic template.
43906
+
43907
+ Body structure (REQUIRED \u2014 the body is split into the agent's system prompt and the first user turn by section headings, so follow this exactly):
43908
+ - Start with the persona/expertise and any Rules as plain prose/paragraphs at the top (no heading needed) \u2014 this becomes the agent's system prompt.
43909
+ - Then the request, using these exact standalone heading lines (letters only, each on its own line, ending with a colon): \`Your task:\`, \`Steps:\`, \`Data needed:\`. Everything under these headings becomes the first user turn.
43910
+ - Headings must match exactly (e.g. \`Your task:\`, not \`Your task (SEO):\`) or the split breaks. Put the business-specific detail in the body text under each heading, never in the heading itself.
43894
43911
 
43895
43912
  2. RECOMMEND (todoforai-cli \u2014 references a template id onto the project; never creates a running todo):
43896
43913
  \`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.
43914
+ Take <projectId> and <businessContextId> from your current session context and pass them explicitly. Put the one-line WHY for THIS business in --note (the card's tooltip). The card appears in the Recommendations column for the user to review before any credits are spent.
43898
43915
 
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.`,
43916
+ 3. Recommend 3-6 cards (highest priority first). Then reply with a one-line summary per card and tell the user they're waiting in the Recommendations column for review.`,
43900
43917
  mcpConfigs: {},
43901
43918
  edgesMcpConfigs: {},
43902
43919
  permissions: {
@@ -43972,12 +43989,16 @@ var TOPICS = {
43972
43989
  channel: (p) => SERVER_TO_FRONTENDS.todo.context_info_updated(p.todoId),
43973
43990
  audience: "frontend"
43974
43991
  },
43992
+ ["predict:reply_updated" /* PREDICTED_REPLY_UPDATED */]: {
43993
+ channel: (p) => SERVER_TO_FRONTENDS.todo.predicted_reply_updated(p.todoId),
43994
+ audience: "frontend"
43995
+ },
43975
43996
  ["hexgrid:updated" /* HEXGRID_UPDATED */]: {
43976
43997
  channel: (p) => SERVER_TO_FRONTENDS.hexGrid.updated(p.projectId),
43977
43998
  audience: "frontend"
43978
43999
  },
43979
- ["recommendation:updated" /* RECOMMENDATION_UPDATED */]: {
43980
- channel: (p) => SERVER_TO_FRONTENDS.recommendation.updated(p.projectId),
44000
+ ["placement:updated" /* PLACEMENT_UPDATED */]: {
44001
+ channel: (p) => SERVER_TO_FRONTENDS.placement.updated(p.projectId),
43981
44002
  audience: "frontend"
43982
44003
  },
43983
44004
  ["sandbox:state" /* SANDBOX_STATE */]: {
@@ -44104,12 +44125,13 @@ Usage:
44104
44125
  todoforai-cli status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
44105
44126
  todoforai-cli delete <todo-id> # Permanently delete a todo
44106
44127
  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')
44128
+ todoforai-cli show <file|-> [todo-id] # Show a file in the chat (rendered by mimetype; - reads stdin)
44129
+ # [--title T] [--alias A] [--mime M] [--json]
44130
+ todoforai-cli open <url> [todo-id] # Show a live http(s) url in the chat as a preview
44131
+ # [--title T] [--alias A] [--json]
44132
+ todoforai-cli recommend --template <id> # Add a template as a recommendation card (see 'todoregistry-cli create')
44108
44133
  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
44134
+ todoforai-cli next [--direction "<text>"] # Ask the analyzer for growth recommendation cards (optional free-text steer)
44113
44135
 
44114
44136
  Options:
44115
44137
  --path <dir> Workspace path (default: cwd)
@@ -44183,6 +44205,9 @@ function parseCliArgs() {
44183
44205
  note: { type: "string" },
44184
44206
  priority: { type: "string" },
44185
44207
  title: { type: "string" },
44208
+ alias: { type: "string" },
44209
+ mime: { type: "string" },
44210
+ direction: { type: "string" },
44186
44211
  "business-context": { type: "string" },
44187
44212
  seed: { type: "string" },
44188
44213
  emails: { type: "string" },
@@ -44840,6 +44865,12 @@ class ScopedConfig {
44840
44865
  get data() {
44841
44866
  return this.store.data.per_api_url[this.apiUrl];
44842
44867
  }
44868
+ clearDefaultProject() {
44869
+ const s = this.data;
44870
+ s.default_project_id = null;
44871
+ s.default_project_name = null;
44872
+ this.store.save();
44873
+ }
44843
44874
  setDefaultProject(id, name) {
44844
44875
  const s = this.data;
44845
44876
  s.default_project_id = id;
@@ -46094,7 +46125,7 @@ Usage:
46094
46125
  <agent> is a name or id (unique partial name also works).
46095
46126
  Fields map directly to agent settings; values are parsed as JSON when possible
46096
46127
  (numbers, booleans), otherwise treated as strings. Common fields:
46097
- model e.g. claude | anthropic:anthropic/claude-opus-4.8
46128
+ model e.g. claude | anthropic:anthropic/claude-opus-5
46098
46129
  ('claude' is the rolling alias \u2192 latest Claude Opus)
46099
46130
  systemMessage freeform prompt text (alias: sysmsg)
46100
46131
  temperature number, e.g. 0.7
@@ -46103,7 +46134,7 @@ Fields map directly to agent settings; values are parsed as JSON when possible
46103
46134
 
46104
46135
  Examples:
46105
46136
  todoforai-cli agent update <agent> model=claude
46106
- todoforai-cli agent update <agent> model=anthropic:anthropic/claude-opus-4.8 temperature=0.5
46137
+ todoforai-cli agent update <agent> model=anthropic:anthropic/claude-opus-5 temperature=0.5
46107
46138
  todoforai-cli agent update <agent> sysmsg="You are a terse video editor."
46108
46139
  `);
46109
46140
  }
@@ -46390,92 +46421,6 @@ async function listTodosCommand(api, defaultProjectId, argv) {
46390
46421
  `);
46391
46422
  }
46392
46423
 
46393
- // src/steering-command.ts
46394
- var RED2 = "\x1B[31m";
46395
- var GREEN2 = "\x1B[32m";
46396
- var DIM2 = "\x1B[2m";
46397
- var RESET2 = "\x1B[0m";
46398
- async function rest(ctx, method, path4, body) {
46399
- const res = await fetch(`${ctx.apiUrl}/api/v1${path4}`, {
46400
- method,
46401
- headers: { "content-type": "application/json", "x-api-key": ctx.apiKey },
46402
- ...body ? { body: JSON.stringify(body) } : {},
46403
- signal: AbortSignal.timeout(30000)
46404
- });
46405
- const text = await res.text();
46406
- if (!res.ok)
46407
- throw new Error(`${method} ${path4} failed: ${res.status} ${text}`);
46408
- return text ? JSON.parse(text) : null;
46409
- }
46410
- function printSteering(s) {
46411
- process.stderr.write(`${GREEN2}Direction:${RESET2} ${s.direction || DIM2 + "(none)" + RESET2}
46412
- `);
46413
- process.stderr.write(`${GREEN2}Boosted:${RESET2} ${s.boostedCategoryIds?.join(", ") || DIM2 + "(none)" + RESET2}
46414
- `);
46415
- process.stderr.write(`${GREEN2}Muted:${RESET2} ${s.mutedCategoryIds?.join(", ") || DIM2 + "(none)" + RESET2}
46416
- `);
46417
- }
46418
- var USAGE = `${RED2}Usage:
46419
- todoforai-cli steering get [--project <id>]
46420
- todoforai-cli steering direction "<text>" [--project <id>] (empty text clears it)
46421
- todoforai-cli steering boost <categoryId> [--project <id>]
46422
- todoforai-cli steering mute <categoryId> [--project <id>]
46423
- todoforai-cli steering clear [--project <id>] (clears all steering)${RESET2}`;
46424
- async function steeringCommand(ctx, argv) {
46425
- const sub = argv[0];
46426
- const path4 = `/projects/${ctx.projectId}/recommendation-steering`;
46427
- if (!sub || sub === "get") {
46428
- const s = await rest(ctx, "GET", path4);
46429
- if (ctx.json)
46430
- console.log(JSON.stringify(s, null, 2));
46431
- else
46432
- printSteering(s);
46433
- return;
46434
- }
46435
- if (sub === "direction") {
46436
- const text = argv.slice(1).join(" ").trim();
46437
- const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, direction: text || null });
46438
- if (ctx.json)
46439
- console.log(JSON.stringify(s, null, 2));
46440
- else
46441
- process.stderr.write(`${GREEN2}\u2705 ${text ? `Direction set: ${text}` : "Direction cleared"}${RESET2}
46442
- `);
46443
- return;
46444
- }
46445
- if (sub === "boost" || sub === "mute") {
46446
- const categoryId = argv[1];
46447
- if (!categoryId) {
46448
- process.stderr.write(USAGE + `
46449
- `);
46450
- process.exit(2);
46451
- }
46452
- const current = await rest(ctx, "GET", path4);
46453
- const key = sub === "boost" ? "boostedCategoryIds" : "mutedCategoryIds";
46454
- const other = sub === "boost" ? "mutedCategoryIds" : "boostedCategoryIds";
46455
- const next = Array.from(new Set([...current[key] ?? [], categoryId]));
46456
- const nextOther = (current[other] ?? []).filter((c2) => c2 !== categoryId);
46457
- const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, [key]: next, [other]: nextOther });
46458
- if (ctx.json)
46459
- console.log(JSON.stringify(s, null, 2));
46460
- else
46461
- process.stderr.write(`${GREEN2}\u2705 ${sub === "boost" ? "Boosted" : "Muted"} ${categoryId}${RESET2}
46462
- `);
46463
- return;
46464
- }
46465
- if (sub === "clear") {
46466
- const s = await rest(ctx, "PATCH", path4, { projectId: ctx.projectId, direction: null, boostedCategoryIds: [], mutedCategoryIds: [] });
46467
- if (ctx.json)
46468
- console.log(JSON.stringify(s, null, 2));
46469
- else
46470
- process.stderr.write(`${GREEN2}\u2705 Steering cleared${RESET2}
46471
- `);
46472
- return;
46473
- }
46474
- process.stderr.write(USAGE + `
46475
- `);
46476
- process.exit(2);
46477
- }
46478
-
46479
46424
  // src/index.ts
46480
46425
  try {
46481
46426
  const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
@@ -46676,8 +46621,7 @@ Cancelled by user (Ctrl+C)
46676
46621
  await deviceLogin();
46677
46622
  return;
46678
46623
  }
46679
- const notDst = (t) => t.startsWith("dst_") ? "" : t;
46680
- let apiKey = args["api-key"] || notDst(readCredential(apiUrl)) || notDst(getEnv("API_TOKEN")) || "";
46624
+ let apiKey = args["api-key"] || readCredential(apiUrl) || getEnv("API_TOKEN") || "";
46681
46625
  if (!apiKey) {
46682
46626
  apiKey = await deviceLogin();
46683
46627
  }
@@ -46721,8 +46665,8 @@ Cancelled by user (Ctrl+C)
46721
46665
  return;
46722
46666
  }
46723
46667
  if (positionals[0] === "addmessage") {
46724
- const [, todoId, ...rest2] = positionals;
46725
- const content2 = rest2.join(" ") || await readStdin();
46668
+ const [, todoId, ...rest] = positionals;
46669
+ const content2 = rest.join(" ") || await readStdin();
46726
46670
  if (!todoId || !content2) {
46727
46671
  process.stderr.write(`${RED}Usage: todoforai-cli addmessage <todo-id> "content"${RESET}
46728
46672
  `);
@@ -46737,6 +46681,55 @@ Cancelled by user (Ctrl+C)
46737
46681
  `);
46738
46682
  return;
46739
46683
  }
46684
+ if (positionals[0] === "show") {
46685
+ const [, filePath, todoArg] = positionals;
46686
+ const todoId = todoArg || getEnv("TODO_ID") || cfgScope.data.last_todo_id;
46687
+ if (!filePath || !todoId) {
46688
+ process.stderr.write(`${RED}Usage: todoforai-cli show <file|-> [todo-id]${RESET}
46689
+ `);
46690
+ process.exit(2);
46691
+ }
46692
+ let blob, name;
46693
+ if (filePath === "-") {
46694
+ blob = new Blob([await Bun.readableStreamToArrayBuffer(Bun.stdin.stream())]);
46695
+ if (blob.size === 0) {
46696
+ process.stderr.write(`${RED}No data on stdin${RESET}
46697
+ `);
46698
+ process.exit(1);
46699
+ }
46700
+ name = "stdin";
46701
+ } else {
46702
+ const file = Bun.file(resolve3(filePath));
46703
+ if (!await file.exists()) {
46704
+ process.stderr.write(`${RED}File not found: ${filePath}${RESET}
46705
+ `);
46706
+ process.exit(1);
46707
+ }
46708
+ blob = file;
46709
+ name = path4.basename(filePath);
46710
+ }
46711
+ const res = await api.showFile(todoId, blob, name, { title: args.title, alias: args.alias, mime: args.mime });
46712
+ if (args.json)
46713
+ console.log(JSON.stringify(res, null, 2));
46714
+ else
46715
+ console.log(res.ref);
46716
+ return;
46717
+ }
46718
+ if (positionals[0] === "open") {
46719
+ const [, url, todoArg] = positionals;
46720
+ const todoId = todoArg || getEnv("TODO_ID") || cfgScope.data.last_todo_id;
46721
+ if (!url || !todoId) {
46722
+ process.stderr.write(`${RED}Usage: todoforai-cli open <url> [todo-id]${RESET}
46723
+ `);
46724
+ process.exit(2);
46725
+ }
46726
+ const res = await api.showUrl(todoId, url, { title: args.title, alias: args.alias });
46727
+ if (args.json)
46728
+ console.log(JSON.stringify(res, null, 2));
46729
+ else
46730
+ console.log(res.ref);
46731
+ return;
46732
+ }
46740
46733
  if (positionals[0] === "recommend") {
46741
46734
  const templateId = args.template || positionals[1];
46742
46735
  if (!templateId) {
@@ -46798,7 +46791,7 @@ Cancelled by user (Ctrl+C)
46798
46791
  process.exit(2);
46799
46792
  }
46800
46793
  }
46801
- const res = await fetch(`${apiUrl}/api/v1/claims/mint`, {
46794
+ const res = await fetch(`${apiUrl}${restBasePath(apiKey)}/claims/mint`, {
46802
46795
  method: "POST",
46803
46796
  headers: { "content-type": "application/json", "x-api-key": apiKey },
46804
46797
  body: JSON.stringify({ seedProjectId, ...emails?.length ? { emails } : {}, ...ttlSec !== undefined ? { ttlSec } : {} }),
@@ -46819,7 +46812,7 @@ Cancelled by user (Ctrl+C)
46819
46812
  `);
46820
46813
  return;
46821
46814
  }
46822
- if (positionals[0] === "steering" || positionals[0] === "next") {
46815
+ if (positionals[0] === "next") {
46823
46816
  let projectId2 = args.project || cfgScope.data.default_project_id;
46824
46817
  if (!projectId2) {
46825
46818
  const projects2 = await api.listProjects();
@@ -46830,29 +46823,25 @@ Cancelled by user (Ctrl+C)
46830
46823
  `);
46831
46824
  process.exit(2);
46832
46825
  }
46833
- const sctx = { apiUrl, apiKey, projectId: projectId2, json: !!args.json };
46834
- if (positionals[0] === "next") {
46835
- const res = await fetch(`${apiUrl}/api/v1/projects/${projectId2}/recommendations/generate`, {
46836
- method: "POST",
46837
- headers: { "content-type": "application/json", "x-api-key": apiKey },
46838
- body: JSON.stringify({ projectId: projectId2, ...args["business-context"] ? { businessContextId: args["business-context"] } : {} }),
46839
- signal: AbortSignal.timeout(30000)
46840
- });
46841
- const text = await res.text();
46842
- if (!res.ok) {
46843
- process.stderr.write(`${RED}Generate failed: ${res.status} ${text}${RESET}
46844
- `);
46845
- process.exit(1);
46846
- }
46847
- const out = JSON.parse(text);
46848
- if (args.json)
46849
- console.log(JSON.stringify(out, null, 2));
46850
- else
46851
- process.stderr.write(`${GREEN}\u2705 Generating recommendations \u2014 analyzer todo ${out.todoId}${RESET}
46826
+ const direction = args.direction || positionals.slice(1).join(" ").trim() || undefined;
46827
+ const res = await fetch(`${apiUrl}${restBasePath(apiKey)}/projects/${projectId2}/recommendations/generate`, {
46828
+ method: "POST",
46829
+ headers: { "content-type": "application/json", "x-api-key": apiKey },
46830
+ body: JSON.stringify({ projectId: projectId2, ...args["business-context"] ? { businessContextId: args["business-context"] } : {}, ...direction ? { direction } : {} }),
46831
+ signal: AbortSignal.timeout(30000)
46832
+ });
46833
+ const text = await res.text();
46834
+ if (!res.ok) {
46835
+ process.stderr.write(`${RED}Generate failed: ${res.status} ${text}${RESET}
46852
46836
  `);
46853
- return;
46837
+ process.exit(1);
46854
46838
  }
46855
- await steeringCommand(sctx, positionals.slice(1));
46839
+ const out = JSON.parse(text);
46840
+ if (args.json)
46841
+ console.log(JSON.stringify(out, null, 2));
46842
+ else
46843
+ process.stderr.write(`${GREEN}\u2705 Generating recommendations \u2014 analyzer todo ${out.todoId}${RESET}
46844
+ `);
46856
46845
  return;
46857
46846
  }
46858
46847
  if (args["list-agents"]) {
@@ -46925,7 +46914,8 @@ Cancelled by user (Ctrl+C)
46925
46914
  const projects2 = await api.listProjects();
46926
46915
  let projectId2 = args.project;
46927
46916
  if (!projectId2) {
46928
- projectId2 = cfgScope.data.default_project_id || projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
46917
+ const cached = cfgScope.data.default_project_id;
46918
+ projectId2 = (cached && projects2.some((p) => getItemId(p) === cached) ? cached : null) || projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
46929
46919
  }
46930
46920
  if (!projectId2) {
46931
46921
  process.stderr.write(`Error: No project found
@@ -47128,7 +47118,18 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
47128
47118
  agent = { ...agent, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
47129
47119
  }
47130
47120
  cfg.addToHistory(content);
47131
- const todo = await api.addMessage(projectId, content, agent);
47121
+ let todo;
47122
+ try {
47123
+ todo = await api.addMessage(projectId, content, agent);
47124
+ } catch (e) {
47125
+ if (!args.project && cfgScope.data.default_project_id === projectId && /failed: 403/.test(e.message || "")) {
47126
+ cfgScope.clearDefaultProject();
47127
+ process.stderr.write(`${RED}Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.${RESET}
47128
+ `);
47129
+ process.exit(1);
47130
+ }
47131
+ throw e;
47132
+ }
47132
47133
  const actualTodoId = todo.id || crypto.randomUUID();
47133
47134
  cfgScope.setLastTodoId(actualTodoId);
47134
47135
  const frontendUrl = getFrontendUrl(apiUrl, projectId, actualTodoId);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "bin": {
6
- "todoforai-cli": "dist/todoai.js",
7
- "todoai": "dist/todoai.js"
6
+ "todoforai-cli": "bin/todoforai-cli.js",
7
+ "todoai": "bin/todoforai-cli.js"
8
8
  },
9
9
  "files": [
10
+ "bin",
10
11
  "dist/todoai.js"
11
12
  ],
12
13
  "scripts": {