@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.
- package/dist/todoai.js +390 -45
- 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: [
|
|
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
|
-
|
|
44555
|
-
|
|
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/
|
|
44788
|
-
import {
|
|
44789
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
44816
|
-
|
|
44817
|
-
|
|
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 = "
|
|
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:
|
|
46423
|
-
if (
|
|
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
|
-
|
|
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, ...
|
|
46517
|
-
const content2 =
|
|
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
|
-
|
|
46804
|
-
|
|
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.
|
|
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",
|