@todoforai/cli 0.1.14 → 0.1.16
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/README.md +22 -19
- package/dist/todoai.js +302 -95
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# todoforai-cli CLI
|
|
2
2
|
|
|
3
3
|
CLI for [TODOforAI](https://todofor.ai) — create, watch, and inspect AI-powered todos.
|
|
4
4
|
|
|
@@ -6,46 +6,48 @@ CLI for [TODOforAI](https://todofor.ai) — create, watch, and inspect AI-powere
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun install -g @todoforai/cli
|
|
9
|
+
# Install the native bridge once, if it is not already on PATH:
|
|
10
|
+
curl -fsSL https://raw.githubusercontent.com/todoforai/bridge/main/install.sh | sh
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## Setup
|
|
12
14
|
|
|
13
|
-
Just run `
|
|
15
|
+
Just run `todoforai-cli` — on first use it opens a browser for **device login** and saves the CLI API key in the shared TODOforAI credentials file. The bridge uses the same file for its own device credentials.
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
todoforai-cli # prompts device login if no key found
|
|
19
|
+
todoforai-cli login # explicit login
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
API URL resolution: `--api-url` flag → `TODOFORAI_API_URL` env → `https://api.todofor.ai`.
|
|
21
23
|
|
|
22
|
-
Auth resolution: `--api-key` flag → `TODOFORAI_API_KEY` env → shared credentials
|
|
24
|
+
Auth resolution: `--api-key` flag → `TODOFORAI_API_KEY` env → shared credentials file → device login.
|
|
23
25
|
|
|
24
26
|
Project, agent, and last-todo state are stored **per API URL** under `per_api_url[<url>]` in the config — switching between e.g. `https://api.todofor.ai` and `http://localhost:4000` keeps each environment's defaults isolated. Legacy top-level fields are auto-migrated on first run.
|
|
25
27
|
|
|
26
|
-
##
|
|
28
|
+
## Bridge
|
|
27
29
|
|
|
28
|
-
The CLI talks to the backend over WebSocket; **shell execution, file I/O, and tool calls happen in the
|
|
30
|
+
The CLI talks to the backend over WebSocket; **shell execution, file I/O, and tool calls happen in the bridge** running locally. On create/resume/template runs, `todoforai-cli` starts a detached `todoforai-bridge` process if needed (the bridge enforces its own single-instance lock, logs at `~/.todoforai/bridge.log`). If bridge credentials are missing, the CLI runs `todoforai-bridge login` in the foreground first so you can see and approve the device-login URL. The bridge keeps running after the CLI exits, so long-running tasks survive `Ctrl+D`.
|
|
29
31
|
|
|
30
|
-
Disable with `--no-
|
|
32
|
+
Disable with `--no-bridge` if you manage the bridge yourself (e.g. systemd, separate terminal). `--no-edge` remains supported as a deprecated alias.
|
|
31
33
|
|
|
32
34
|
## Usage
|
|
33
35
|
|
|
34
36
|
### Create a todo from a prompt
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
echo "content" |
|
|
40
|
-
|
|
39
|
+
todoforai-cli "Fix the login bug"
|
|
40
|
+
todoforai-cli -n "Quick task" # non-interactive (run and exit)
|
|
41
|
+
echo "content" | todoforai-cli # pipe from stdin
|
|
42
|
+
todoforai-cli --path /my/project "Fix bug" # explicit workspace
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
### Start from a registry template
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
todoforai-cli --template alternativeto-listing # interactive input prompts
|
|
49
|
+
todoforai-cli --template f5bot-monitoring-setup --input "monitoring_details=My Brand" # with inputs
|
|
50
|
+
todoforai-cli --template f5bot-monitoring-setup --no-watch --json # create only
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
When inputs are missing, the CLI prompts interactively (unless `-n`).
|
|
@@ -53,7 +55,7 @@ When inputs are missing, the CLI prompts interactively (unless `-n`).
|
|
|
53
55
|
### Inspect a todo (read-only)
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
|
|
58
|
+
todoforai-cli --inspect <todo-id>
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
Prints the full chat log: messages, tool calls (type, status, path/cmd), results, and errors. No logo, no interactive mode.
|
|
@@ -61,8 +63,8 @@ Prints the full chat log: messages, tool calls (type, status, path/cmd), results
|
|
|
61
63
|
### Resume / continue
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
todoforai-cli -c # continue most recent todo
|
|
67
|
+
todoforai-cli --resume <todo-id> # resume specific todo
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
## All Options
|
|
@@ -82,7 +84,8 @@ todoai --resume <todo-id> # resume specific todo
|
|
|
82
84
|
--dangerously-skip-permissions Auto-approve all blocks (CI/benchmarks)
|
|
83
85
|
--allow-all Set permissions to allow all tools (no approval needed)
|
|
84
86
|
--no-watch Create todo and exit
|
|
85
|
-
--no-
|
|
87
|
+
--no-bridge Do not auto-spawn bridge
|
|
88
|
+
--no-edge Deprecated alias for --no-bridge
|
|
86
89
|
--json Output as JSON
|
|
87
90
|
--safe Validate API key upfront
|
|
88
91
|
--debug, -d Debug output
|
package/dist/todoai.js
CHANGED
|
@@ -42752,6 +42752,27 @@ class ApiClient {
|
|
|
42752
42752
|
throw new Error(`API ${method} ${endpoint} failed: ${res.status} ${await res.text()}`);
|
|
42753
42753
|
return res.json();
|
|
42754
42754
|
}
|
|
42755
|
+
async trpcQuery(path2, input) {
|
|
42756
|
+
const base = this.apiUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
|
|
42757
|
+
const params = new URLSearchParams({
|
|
42758
|
+
batch: "1",
|
|
42759
|
+
input: JSON.stringify({ 0: input })
|
|
42760
|
+
});
|
|
42761
|
+
const res = await fetch(`${base}/trpc/api/${path2}?${params}`, {
|
|
42762
|
+
method: "GET",
|
|
42763
|
+
headers: this.headers,
|
|
42764
|
+
signal: AbortSignal.timeout(30000)
|
|
42765
|
+
});
|
|
42766
|
+
const text = await res.text();
|
|
42767
|
+
if (!res.ok)
|
|
42768
|
+
throw new Error(`API tRPC ${path2} failed: ${res.status} ${text}`);
|
|
42769
|
+
const payload = JSON.parse(text);
|
|
42770
|
+
const item = Array.isArray(payload) ? payload[0] : payload;
|
|
42771
|
+
if (item?.error)
|
|
42772
|
+
throw new Error(`API tRPC ${path2} failed: ${item.error.message || JSON.stringify(item.error)}`);
|
|
42773
|
+
const data = item?.result?.data;
|
|
42774
|
+
return data?.json !== undefined ? data.json : data;
|
|
42775
|
+
}
|
|
42755
42776
|
async validateApiKey() {
|
|
42756
42777
|
if (!this.apiKey)
|
|
42757
42778
|
return { valid: false, error: "No API key provided" };
|
|
@@ -42790,9 +42811,10 @@ class ApiClient {
|
|
|
42790
42811
|
createTodo(projectId, content, agentSettings) {
|
|
42791
42812
|
return this.request("POST", `/api/v1/projects/${projectId}/todos`, { content, agentSettings });
|
|
42792
42813
|
}
|
|
42793
|
-
listTodos(projectId) {
|
|
42794
|
-
|
|
42795
|
-
|
|
42814
|
+
listTodos(projectId, opts) {
|
|
42815
|
+
if (!projectId)
|
|
42816
|
+
return this.request("GET", "/api/v1/todos");
|
|
42817
|
+
return this.trpcQuery("todo.list", { projectId, ...opts });
|
|
42796
42818
|
}
|
|
42797
42819
|
getTodo(todoId) {
|
|
42798
42820
|
return this.request("GET", `/api/v1/todos/${todoId}`);
|
|
@@ -43107,8 +43129,8 @@ var package_default = {
|
|
|
43107
43129
|
version: "0.1.3",
|
|
43108
43130
|
type: "module",
|
|
43109
43131
|
bin: {
|
|
43110
|
-
|
|
43111
|
-
|
|
43132
|
+
"todoforai-cli": "dist/todoai.js",
|
|
43133
|
+
todoai: "dist/todoai.js"
|
|
43112
43134
|
},
|
|
43113
43135
|
files: ["dist/todoai.js"],
|
|
43114
43136
|
scripts: {
|
|
@@ -43147,6 +43169,7 @@ var TodoStatus;
|
|
|
43147
43169
|
TodoStatus2["POSTPONED"] = "POSTPONED";
|
|
43148
43170
|
TodoStatus2["REVIEW_REQUESTED"] = "REVIEW_REQUESTED";
|
|
43149
43171
|
TodoStatus2["RUNNING"] = "RUNNING";
|
|
43172
|
+
TodoStatus2["WAITING_FOR_USER"] = "WAITING_FOR_USER";
|
|
43150
43173
|
TodoStatus2["COMPACTING"] = "COMPACTING";
|
|
43151
43174
|
TodoStatus2["STOPPING"] = "STOPPING";
|
|
43152
43175
|
TodoStatus2["READY"] = "READY";
|
|
@@ -43451,6 +43474,15 @@ var SERVER_TO_FRONTENDS = {
|
|
|
43451
43474
|
},
|
|
43452
43475
|
functions: {}
|
|
43453
43476
|
};
|
|
43477
|
+
// ../packages/shared-fbe/src/templateBody.ts
|
|
43478
|
+
var USER_HEADINGS = new Set([
|
|
43479
|
+
"your task",
|
|
43480
|
+
"your tasks",
|
|
43481
|
+
"tasks",
|
|
43482
|
+
"task",
|
|
43483
|
+
"steps",
|
|
43484
|
+
"data needed"
|
|
43485
|
+
]);
|
|
43454
43486
|
// ../packages/shared-fbe/src/bashPatterns.ts
|
|
43455
43487
|
function splitShellCommands(input) {
|
|
43456
43488
|
const parts = [];
|
|
@@ -43940,24 +43972,24 @@ function getEnv(name) {
|
|
|
43940
43972
|
}
|
|
43941
43973
|
function printUsage() {
|
|
43942
43974
|
process.stderr.write(`
|
|
43943
|
-
|
|
43975
|
+
todoforai-cli \u2014 TODOforAI CLI (Bun)
|
|
43944
43976
|
|
|
43945
43977
|
Usage:
|
|
43946
|
-
|
|
43947
|
-
|
|
43948
|
-
|
|
43949
|
-
echo "content" |
|
|
43950
|
-
|
|
43951
|
-
|
|
43952
|
-
|
|
43953
|
-
|
|
43954
|
-
|
|
43955
|
-
|
|
43956
|
-
|
|
43957
|
-
|
|
43958
|
-
|
|
43959
|
-
|
|
43960
|
-
|
|
43978
|
+
todoforai-cli login # Browser-based device auth
|
|
43979
|
+
todoforai-cli "prompt text" # Prompt as argument
|
|
43980
|
+
todoforai-cli -n "Quick task" # Non-interactive (run and exit)
|
|
43981
|
+
echo "content" | todoforai-cli # Pipe from stdin
|
|
43982
|
+
todoforai-cli --path /my/project "Fix bug" # Explicit workspace path
|
|
43983
|
+
todoforai-cli -c ["prompt"] # Resume last todo (optional prompt sent on attach)
|
|
43984
|
+
todoforai-cli --resume <todo-id> ["prompt"] # Resume specific todo (optional prompt sent on attach)
|
|
43985
|
+
todoforai-cli --inspect <todo-id>[@<slice>] # Read chat log. <slice> = -3:, :1, 5:10, 7 (Python-style)
|
|
43986
|
+
todoforai-cli --template <id> [--input k=v] # Start from a registry template
|
|
43987
|
+
todoforai-cli --list-agents # List available agents and exit
|
|
43988
|
+
todoforai-cli agent update <agent> model=<model> # Update agent settings (see 'agent --help')
|
|
43989
|
+
todoforai-cli list [-n 30] [--cursor N] [--all] [--status S] # List todos (paginated); see 'list --help'
|
|
43990
|
+
todoforai-cli status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
|
|
43991
|
+
todoforai-cli delete <todo-id> # Permanently delete a todo
|
|
43992
|
+
todoforai-cli addmessage <todo-id> "text" # Add a message to an existing todo
|
|
43961
43993
|
|
|
43962
43994
|
Options:
|
|
43963
43995
|
--path <dir> Workspace path (default: cwd)
|
|
@@ -43976,7 +44008,8 @@ Options:
|
|
|
43976
44008
|
--allow-all Set permissions to allow all tools (no approval needed)
|
|
43977
44009
|
--raw-sysmsg <file> Use file contents verbatim as system prompt (new TODO only)
|
|
43978
44010
|
--no-watch Create todo and exit
|
|
43979
|
-
--no-
|
|
44011
|
+
--no-bridge Do not auto-spawn bridge
|
|
44012
|
+
--no-edge Deprecated alias for --no-bridge
|
|
43980
44013
|
--json Output as JSON
|
|
43981
44014
|
--detailed 'inspect --json': keep ids, timestamps, agentSettingsId, scheduledTimestamp
|
|
43982
44015
|
--format-anthropic 'inspect --json': Anthropic-style shape (tool_result in next user msg); attachment sources are uri-typed, so not a 1:1 messages.create input
|
|
@@ -43999,7 +44032,7 @@ var STATUS_HELP = {
|
|
|
43999
44032
|
};
|
|
44000
44033
|
function printStatusHelp() {
|
|
44001
44034
|
process.stderr.write(`
|
|
44002
|
-
|
|
44035
|
+
todoforai-cli status <todo-id> <STATUS>
|
|
44003
44036
|
|
|
44004
44037
|
Common statuses:
|
|
44005
44038
|
${Object.entries(STATUS_HELP).map(([s, d]) => ` ${s.padEnd(18)}${d}`).join(`
|
|
@@ -44030,6 +44063,7 @@ function parseCliArgs() {
|
|
|
44030
44063
|
"allow-all": { type: "boolean", default: false },
|
|
44031
44064
|
"raw-sysmsg": { type: "string" },
|
|
44032
44065
|
"no-watch": { type: "boolean", default: false },
|
|
44066
|
+
"no-bridge": { type: "boolean", default: false },
|
|
44033
44067
|
"no-edge": { type: "boolean", default: false },
|
|
44034
44068
|
json: { type: "boolean", default: false },
|
|
44035
44069
|
detailed: { type: "boolean", default: false },
|
|
@@ -44045,6 +44079,8 @@ function parseCliArgs() {
|
|
|
44045
44079
|
allowPositionals: true,
|
|
44046
44080
|
strict: false
|
|
44047
44081
|
});
|
|
44082
|
+
if (values["no-edge"])
|
|
44083
|
+
values["no-bridge"] = true;
|
|
44048
44084
|
return { values, positionals };
|
|
44049
44085
|
}
|
|
44050
44086
|
|
|
@@ -45027,6 +45063,28 @@ function getItemId(item) {
|
|
|
45027
45063
|
return item.project.id;
|
|
45028
45064
|
return item?.id || "";
|
|
45029
45065
|
}
|
|
45066
|
+
function resolveAgentMatch(agents, query) {
|
|
45067
|
+
const byId = agents.find((a) => getItemId(a) === query);
|
|
45068
|
+
if (byId)
|
|
45069
|
+
return { match: byId };
|
|
45070
|
+
const q = query.toLowerCase();
|
|
45071
|
+
const esc = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45072
|
+
const boundary = new RegExp(`\\b${esc}`);
|
|
45073
|
+
const name = (a) => getDisplayName(a).toLowerCase();
|
|
45074
|
+
const tiers = [
|
|
45075
|
+
agents.filter((a) => name(a) === q),
|
|
45076
|
+
agents.filter((a) => name(a).startsWith(q)),
|
|
45077
|
+
agents.filter((a) => boundary.test(name(a))),
|
|
45078
|
+
agents.filter((a) => name(a).includes(q))
|
|
45079
|
+
];
|
|
45080
|
+
for (const tier of tiers) {
|
|
45081
|
+
if (tier.length === 1)
|
|
45082
|
+
return { match: tier[0] };
|
|
45083
|
+
if (tier.length > 1)
|
|
45084
|
+
return { ambiguous: tier };
|
|
45085
|
+
}
|
|
45086
|
+
return {};
|
|
45087
|
+
}
|
|
45030
45088
|
function terminalLine(prompt) {
|
|
45031
45089
|
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
45032
45090
|
return new Promise((resolve3) => {
|
|
@@ -45758,12 +45816,12 @@ async function listAgentsCommand(api, opts) {
|
|
|
45758
45816
|
// src/agent-command.ts
|
|
45759
45817
|
function printAgentHelp() {
|
|
45760
45818
|
process.stderr.write(`
|
|
45761
|
-
|
|
45819
|
+
todoforai-cli agent \u2014 inspect and update agent settings
|
|
45762
45820
|
|
|
45763
45821
|
Usage:
|
|
45764
|
-
|
|
45765
|
-
|
|
45766
|
-
|
|
45822
|
+
todoforai-cli agent list List agents (name, model, id, paths)
|
|
45823
|
+
todoforai-cli agent get <agent> Show a single agent's settings
|
|
45824
|
+
todoforai-cli agent update <agent> <field=value>\u2026 Update one or more settings
|
|
45767
45825
|
|
|
45768
45826
|
<agent> is a name or id (unique partial name also works).
|
|
45769
45827
|
Fields map directly to agent settings; values are parsed as JSON when possible
|
|
@@ -45776,9 +45834,9 @@ Fields map directly to agent settings; values are parsed as JSON when possible
|
|
|
45776
45834
|
name agent display name
|
|
45777
45835
|
|
|
45778
45836
|
Examples:
|
|
45779
|
-
|
|
45780
|
-
|
|
45781
|
-
|
|
45837
|
+
todoforai-cli agent update <agent> model=claude
|
|
45838
|
+
todoforai-cli agent update <agent> model=anthropic:anthropic/claude-opus-4.8 temperature=0.5
|
|
45839
|
+
todoforai-cli agent update <agent> sysmsg="You are a terse video editor."
|
|
45782
45840
|
`);
|
|
45783
45841
|
}
|
|
45784
45842
|
var FIELD_ALIASES = {
|
|
@@ -45804,20 +45862,13 @@ function parseAssignment(arg) {
|
|
|
45804
45862
|
}
|
|
45805
45863
|
}
|
|
45806
45864
|
function resolveAgent(agents, query) {
|
|
45807
|
-
const
|
|
45808
|
-
if (
|
|
45809
|
-
return
|
|
45810
|
-
|
|
45811
|
-
|
|
45812
|
-
if (exact.length === 1)
|
|
45813
|
-
return exact[0];
|
|
45814
|
-
const partial = agents.filter((a) => getDisplayName(a).toLowerCase().includes(q));
|
|
45815
|
-
if (partial.length === 1)
|
|
45816
|
-
return partial[0];
|
|
45817
|
-
if (partial.length > 1) {
|
|
45818
|
-
process.stderr.write(`${RED}Ambiguous agent '${query}', matches:${RESET}
|
|
45865
|
+
const { match, ambiguous } = resolveAgentMatch(agents, query);
|
|
45866
|
+
if (match)
|
|
45867
|
+
return match;
|
|
45868
|
+
if (ambiguous) {
|
|
45869
|
+
process.stderr.write(`${RED}Ambiguous agent '${query}' \u2014 ${ambiguous.length} matches. Re-run with the exact id:${RESET}
|
|
45819
45870
|
`);
|
|
45820
|
-
for (const a of
|
|
45871
|
+
for (const a of ambiguous)
|
|
45821
45872
|
process.stderr.write(` ${getDisplayName(a)} ${DIM}${getItemId(a)}${RESET}
|
|
45822
45873
|
`);
|
|
45823
45874
|
process.exit(2);
|
|
@@ -45853,7 +45904,7 @@ async function agentCommand(api, positionals, args, formatPath) {
|
|
|
45853
45904
|
const query = positionals[2];
|
|
45854
45905
|
const assignments = positionals.slice(3);
|
|
45855
45906
|
if (!query || !assignments.length) {
|
|
45856
|
-
process.stderr.write(`${RED}Usage:
|
|
45907
|
+
process.stderr.write(`${RED}Usage: todoforai-cli agent update <name|id> <field=value>\u2026${RESET}
|
|
45857
45908
|
`);
|
|
45858
45909
|
process.exit(2);
|
|
45859
45910
|
}
|
|
@@ -45907,34 +45958,87 @@ var CLOSED = new Set([
|
|
|
45907
45958
|
]);
|
|
45908
45959
|
var VALID_STATUS = new Set(Object.values(TodoStatus));
|
|
45909
45960
|
var isOpen = (s) => !CLOSED.has(s);
|
|
45910
|
-
function
|
|
45961
|
+
function printListTodosHelp() {
|
|
45911
45962
|
process.stderr.write(`
|
|
45912
|
-
|
|
45963
|
+
todoforai-cli list \u2014 list todos in a project (recent first)
|
|
45913
45964
|
|
|
45914
45965
|
Usage:
|
|
45915
|
-
|
|
45966
|
+
todoforai-cli list [flags]
|
|
45916
45967
|
|
|
45917
45968
|
Flags:
|
|
45918
45969
|
-n, --limit <n> Max rows to show (default: 30)
|
|
45970
|
+
--cursor <n> Fetch todos older than this cursor (lastActivityAt)
|
|
45971
|
+
--page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
|
|
45972
|
+
--search <text> Search content/status on the backend
|
|
45919
45973
|
-s, --status <S[,S2]> Filter by status (comma-separated, union).
|
|
45920
45974
|
-A, --all Include DONE (also CANCELLED, ARCHIVED, \u2026)
|
|
45921
45975
|
--project <id> Project ID (default: current default project)
|
|
45922
|
-
--json Output
|
|
45976
|
+
--json Output { items, nextCursor } as JSON
|
|
45923
45977
|
-h, --help Show this help
|
|
45924
45978
|
|
|
45925
45979
|
Examples:
|
|
45926
|
-
|
|
45927
|
-
|
|
45928
|
-
|
|
45929
|
-
|
|
45930
|
-
|
|
45980
|
+
todoforai-cli list # 30 most recent open todos
|
|
45981
|
+
todoforai-cli list -n 50 # last 50 open
|
|
45982
|
+
todoforai-cli list --cursor 1719234567890 # next page
|
|
45983
|
+
todoforai-cli list --all # include DONE
|
|
45984
|
+
todoforai-cli list -s RUNNING,REVIEW_REQUESTED
|
|
45985
|
+
todoforai-cli list --search bug --json | jq '.items[].id'
|
|
45986
|
+
`);
|
|
45987
|
+
}
|
|
45988
|
+
function positiveInt(value, name) {
|
|
45989
|
+
if (value === undefined)
|
|
45990
|
+
return;
|
|
45991
|
+
const n = Number(value);
|
|
45992
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
45993
|
+
process.stderr.write(`${RED}Error: ${name} must be a positive integer${RESET}
|
|
45931
45994
|
`);
|
|
45995
|
+
process.exit(2);
|
|
45996
|
+
}
|
|
45997
|
+
return n;
|
|
45998
|
+
}
|
|
45999
|
+
function cursorValue(value) {
|
|
46000
|
+
if (value === undefined)
|
|
46001
|
+
return;
|
|
46002
|
+
const n = Number(value);
|
|
46003
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
46004
|
+
process.stderr.write(`${RED}Error: --cursor must be a non-negative number${RESET}
|
|
46005
|
+
`);
|
|
46006
|
+
process.exit(2);
|
|
46007
|
+
}
|
|
46008
|
+
return n;
|
|
46009
|
+
}
|
|
46010
|
+
function toItemsPage(response) {
|
|
46011
|
+
if (Array.isArray(response))
|
|
46012
|
+
return { items: response };
|
|
46013
|
+
return {
|
|
46014
|
+
items: Array.isArray(response?.items) ? response.items : [],
|
|
46015
|
+
nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
|
|
46016
|
+
};
|
|
46017
|
+
}
|
|
46018
|
+
function rowTime(t) {
|
|
46019
|
+
return Number(t.lastActivityAt ?? t.createdAt ?? 0);
|
|
46020
|
+
}
|
|
46021
|
+
function printStatusError(unknown) {
|
|
46022
|
+
process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
|
|
46023
|
+
`);
|
|
46024
|
+
process.stderr.write(`${DIM}Use OPEN or one of: ${Object.values(TodoStatus).join(", ")}${RESET}
|
|
46025
|
+
`);
|
|
46026
|
+
process.exit(2);
|
|
46027
|
+
}
|
|
46028
|
+
function matchesRequestedStatus(t, requested, wanted, wantOpen) {
|
|
46029
|
+
if (!requested)
|
|
46030
|
+
return true;
|
|
46031
|
+
const s = String(t.status).toUpperCase();
|
|
46032
|
+
return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
|
|
45932
46033
|
}
|
|
45933
46034
|
async function listTodosCommand(api, defaultProjectId, argv) {
|
|
45934
46035
|
const { values } = parseArgs2({
|
|
45935
46036
|
args: argv,
|
|
45936
46037
|
options: {
|
|
45937
46038
|
limit: { type: "string", short: "n" },
|
|
46039
|
+
cursor: { type: "string" },
|
|
46040
|
+
"page-size": { type: "string" },
|
|
46041
|
+
search: { type: "string" },
|
|
45938
46042
|
status: { type: "string", short: "s" },
|
|
45939
46043
|
all: { type: "boolean", short: "A", default: false },
|
|
45940
46044
|
project: { type: "string" },
|
|
@@ -45944,7 +46048,7 @@ async function listTodosCommand(api, defaultProjectId, argv) {
|
|
|
45944
46048
|
strict: true
|
|
45945
46049
|
});
|
|
45946
46050
|
if (values.help) {
|
|
45947
|
-
|
|
46051
|
+
printListTodosHelp();
|
|
45948
46052
|
return;
|
|
45949
46053
|
}
|
|
45950
46054
|
const projectId = values.project || defaultProjectId;
|
|
@@ -45953,65 +46057,156 @@ async function listTodosCommand(api, defaultProjectId, argv) {
|
|
|
45953
46057
|
`);
|
|
45954
46058
|
process.exit(1);
|
|
45955
46059
|
}
|
|
45956
|
-
const limit = values.limit
|
|
46060
|
+
const limit = positiveInt(values.limit, "--limit") ?? 30;
|
|
46061
|
+
const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
|
|
46062
|
+
let cursor = cursorValue(values.cursor);
|
|
46063
|
+
const search = values.search ? String(values.search) : undefined;
|
|
45957
46064
|
const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
|
|
45958
|
-
const
|
|
46065
|
+
const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
|
|
46066
|
+
if (unknown.length)
|
|
46067
|
+
printStatusError(unknown);
|
|
46068
|
+
const wantOpen = !!requested?.some((s) => s === "OPEN");
|
|
45959
46069
|
const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
|
|
45960
46070
|
const includeClosed = !!values.all || !!wanted && wanted.size > 0;
|
|
45961
|
-
const
|
|
45962
|
-
let
|
|
45963
|
-
|
|
45964
|
-
|
|
45965
|
-
const
|
|
45966
|
-
|
|
45967
|
-
|
|
46071
|
+
const rows = [];
|
|
46072
|
+
let backendNextCursor;
|
|
46073
|
+
while (rows.length < limit) {
|
|
46074
|
+
const requestLimit = Math.min(pageSize, limit - rows.length);
|
|
46075
|
+
const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
|
|
46076
|
+
backendNextCursor = page.nextCursor;
|
|
46077
|
+
for (const t of page.items) {
|
|
46078
|
+
const status = String(t.status).toUpperCase();
|
|
46079
|
+
if (!includeClosed && CLOSED.has(status))
|
|
46080
|
+
continue;
|
|
46081
|
+
if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
|
|
46082
|
+
continue;
|
|
46083
|
+
rows.push(t);
|
|
46084
|
+
if (rows.length >= limit)
|
|
46085
|
+
break;
|
|
46086
|
+
}
|
|
46087
|
+
if (!backendNextCursor || page.items.length === 0)
|
|
46088
|
+
break;
|
|
46089
|
+
cursor = backendNextCursor;
|
|
46090
|
+
}
|
|
46091
|
+
rows.sort((a, b) => rowTime(b) - rowTime(a));
|
|
46092
|
+
const visible = rows.slice(0, limit);
|
|
46093
|
+
const hasBufferedRows = rows.length > visible.length;
|
|
46094
|
+
const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
|
|
45968
46095
|
if (values.json) {
|
|
45969
|
-
process.stdout.write(JSON.stringify(
|
|
46096
|
+
process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
|
|
45970
46097
|
`);
|
|
45971
46098
|
return;
|
|
45972
46099
|
}
|
|
45973
|
-
if (!
|
|
46100
|
+
if (!visible.length) {
|
|
45974
46101
|
process.stderr.write(`${DIM}(no todos)${RESET}
|
|
46102
|
+
`);
|
|
46103
|
+
if (nextCursor !== undefined)
|
|
46104
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
45975
46105
|
`);
|
|
45976
46106
|
return;
|
|
45977
46107
|
}
|
|
45978
|
-
const statusW =
|
|
45979
|
-
for (const t of
|
|
45980
|
-
const
|
|
45981
|
-
const
|
|
46108
|
+
const statusW = visible.reduce((n, r) => Math.max(n, String(r.status).length), 0);
|
|
46109
|
+
for (const t of visible) {
|
|
46110
|
+
const status = String(t.status).toUpperCase();
|
|
46111
|
+
const sc = STATUS_COLOR[status] || DIM;
|
|
46112
|
+
const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
|
|
45982
46113
|
const title = String(t.content ?? "").split(`
|
|
45983
46114
|
`)[0].slice(0, 100);
|
|
45984
|
-
process.stdout.write(`${sc}${
|
|
46115
|
+
process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
|
|
45985
46116
|
`);
|
|
45986
46117
|
}
|
|
45987
|
-
process.stderr.write(`${DIM}${
|
|
46118
|
+
process.stderr.write(`${DIM}${visible.length} todo(s)${RESET}
|
|
46119
|
+
`);
|
|
46120
|
+
if (nextCursor !== undefined)
|
|
46121
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
45988
46122
|
`);
|
|
45989
46123
|
}
|
|
45990
46124
|
|
|
45991
|
-
// src/ensure-
|
|
46125
|
+
// src/ensure-bridge.ts
|
|
45992
46126
|
import { spawn, spawnSync } from "child_process";
|
|
45993
46127
|
import fs2 from "fs";
|
|
45994
46128
|
import path3 from "path";
|
|
45995
46129
|
import os3 from "os";
|
|
45996
|
-
function
|
|
45997
|
-
const probe = spawnSync(
|
|
46130
|
+
function hasBridge() {
|
|
46131
|
+
const probe = spawnSync("todoforai-bridge", ["--version"], { stdio: "ignore" });
|
|
45998
46132
|
return probe.status === 0;
|
|
45999
46133
|
}
|
|
46000
|
-
function
|
|
46001
|
-
|
|
46002
|
-
|
|
46134
|
+
function isLocalHost(hostname) {
|
|
46135
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
46136
|
+
}
|
|
46137
|
+
function parseApiUrl(apiUrl) {
|
|
46138
|
+
try {
|
|
46139
|
+
return new URL(apiUrl);
|
|
46140
|
+
} catch {
|
|
46141
|
+
return null;
|
|
46142
|
+
}
|
|
46143
|
+
}
|
|
46144
|
+
function bridgeProfile(apiUrl) {
|
|
46145
|
+
const url = parseApiUrl(apiUrl);
|
|
46146
|
+
if (!url)
|
|
46147
|
+
return null;
|
|
46148
|
+
if (isLocalHost(url.hostname))
|
|
46149
|
+
return "dev";
|
|
46150
|
+
if (!url.hostname || url.hostname === "api.todofor.ai")
|
|
46151
|
+
return null;
|
|
46152
|
+
return `api_${url.hostname.replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
46153
|
+
}
|
|
46154
|
+
function withProfile(args, apiUrl) {
|
|
46155
|
+
const profile = bridgeProfile(apiUrl);
|
|
46156
|
+
return profile ? [...args, "--profile", profile] : args;
|
|
46157
|
+
}
|
|
46158
|
+
function bridgeRunArgs(apiUrl) {
|
|
46159
|
+
const url = parseApiUrl(apiUrl);
|
|
46160
|
+
if (!url)
|
|
46161
|
+
return [];
|
|
46162
|
+
if (isLocalHost(url.hostname)) {
|
|
46163
|
+
const args = ["--host", url.hostname];
|
|
46164
|
+
if (url.port)
|
|
46165
|
+
args.push("--port", url.port);
|
|
46166
|
+
return withProfile(args, apiUrl);
|
|
46167
|
+
}
|
|
46168
|
+
if (url.hostname && url.hostname !== "api.todofor.ai")
|
|
46169
|
+
return withProfile(["--host", url.hostname], apiUrl);
|
|
46170
|
+
return [];
|
|
46171
|
+
}
|
|
46172
|
+
function bridgeLoginArgs(apiUrl) {
|
|
46173
|
+
const url = parseApiUrl(apiUrl);
|
|
46174
|
+
if (!url)
|
|
46175
|
+
return ["login"];
|
|
46176
|
+
if (url.hostname && url.hostname !== "api.todofor.ai")
|
|
46177
|
+
return withProfile(["login", "--host", url.hostname], apiUrl);
|
|
46178
|
+
return ["login"];
|
|
46179
|
+
}
|
|
46180
|
+
function bridgeWhoamiArgs(apiUrl) {
|
|
46181
|
+
return withProfile(["whoami"], apiUrl);
|
|
46182
|
+
}
|
|
46183
|
+
function ensureBridgeCredentials(apiUrl) {
|
|
46184
|
+
const whoami = spawnSync("todoforai-bridge", bridgeWhoamiArgs(apiUrl), { stdio: "ignore" });
|
|
46185
|
+
if (whoami.status === 0)
|
|
46186
|
+
return true;
|
|
46187
|
+
console.error("\x1B[2mBridge credentials not found. Starting `todoforai-bridge login`...\x1B[0m");
|
|
46188
|
+
const login = spawnSync("todoforai-bridge", bridgeLoginArgs(apiUrl), { stdio: "inherit" });
|
|
46189
|
+
return login.status === 0;
|
|
46190
|
+
}
|
|
46191
|
+
function ensureBridgeRunning(apiUrl, _apiKey) {
|
|
46192
|
+
if (!hasBridge()) {
|
|
46193
|
+
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");
|
|
46194
|
+
return;
|
|
46195
|
+
}
|
|
46196
|
+
if (!ensureBridgeCredentials(apiUrl)) {
|
|
46197
|
+
console.error("\x1B[33mBridge not started: `todoforai-bridge login` did not complete successfully.\x1B[0m");
|
|
46003
46198
|
return;
|
|
46004
46199
|
}
|
|
46005
46200
|
const logDir = path3.join(os3.homedir(), ".todoforai");
|
|
46006
46201
|
fs2.mkdirSync(logDir, { recursive: true });
|
|
46007
|
-
const logFile = path3.join(logDir, "
|
|
46202
|
+
const logFile = path3.join(logDir, "bridge.log");
|
|
46008
46203
|
const out = fs2.openSync(logFile, "a");
|
|
46009
|
-
const child = spawn("
|
|
46204
|
+
const child = spawn("todoforai-bridge", bridgeRunArgs(apiUrl), {
|
|
46010
46205
|
detached: true,
|
|
46011
46206
|
stdio: ["ignore", out, out]
|
|
46012
46207
|
});
|
|
46013
46208
|
child.on("error", (err) => {
|
|
46014
|
-
console.error(`\x1B[33mFailed to start
|
|
46209
|
+
console.error(`\x1B[33mFailed to start bridge: ${err.message}\x1B[0m`);
|
|
46015
46210
|
});
|
|
46016
46211
|
let exited = false;
|
|
46017
46212
|
let exitCode = null;
|
|
@@ -46026,13 +46221,13 @@ function ensureEdgeRunning(apiUrl, apiKey) {
|
|
|
46026
46221
|
const shortLog = logFile.replace(os3.homedir(), "~");
|
|
46027
46222
|
setTimeout(() => {
|
|
46028
46223
|
if (!exited) {
|
|
46029
|
-
console.error(`\x1B[2mStarted
|
|
46224
|
+
console.error(`\x1B[2mStarted bridge (pid ${pid}), logs: ${shortLog}\x1B[0m`);
|
|
46030
46225
|
return;
|
|
46031
46226
|
}
|
|
46032
46227
|
if (exitCode === 0) {
|
|
46033
|
-
console.error(`\x1B[
|
|
46228
|
+
console.error(`\x1B[2mBridge exited cleanly. Logs: ${shortLog}\x1B[0m`);
|
|
46034
46229
|
} else {
|
|
46035
|
-
console.error(`\x1B[
|
|
46230
|
+
console.error(`\x1B[33mBridge exited early (exit ${exitCode}). Check logs: ${shortLog}. Another instance may already be running.\x1B[0m`);
|
|
46036
46231
|
}
|
|
46037
46232
|
}, 500);
|
|
46038
46233
|
}
|
|
@@ -46139,6 +46334,10 @@ Cancelled by user (Ctrl+C)
|
|
|
46139
46334
|
printAgentHelp();
|
|
46140
46335
|
process.exit(0);
|
|
46141
46336
|
}
|
|
46337
|
+
if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
|
|
46338
|
+
printListTodosHelp();
|
|
46339
|
+
process.exit(0);
|
|
46340
|
+
}
|
|
46142
46341
|
if (args.help && !["list", "ls", "agent"].includes(positionals[0])) {
|
|
46143
46342
|
printUsage();
|
|
46144
46343
|
process.exit(0);
|
|
@@ -46234,7 +46433,7 @@ Cancelled by user (Ctrl+C)
|
|
|
46234
46433
|
if (positionals[0] === "delete") {
|
|
46235
46434
|
const todoId = positionals[1];
|
|
46236
46435
|
if (!todoId) {
|
|
46237
|
-
process.stderr.write(`${RED}Usage:
|
|
46436
|
+
process.stderr.write(`${RED}Usage: todoforai-cli delete <todo-id>${RESET}
|
|
46238
46437
|
`);
|
|
46239
46438
|
process.exit(2);
|
|
46240
46439
|
}
|
|
@@ -46247,7 +46446,7 @@ Cancelled by user (Ctrl+C)
|
|
|
46247
46446
|
const [, todoId, ...rest] = positionals;
|
|
46248
46447
|
const content2 = rest.join(" ") || await readStdin();
|
|
46249
46448
|
if (!todoId || !content2) {
|
|
46250
|
-
process.stderr.write(`${RED}Usage:
|
|
46449
|
+
process.stderr.write(`${RED}Usage: todoforai-cli addmessage <todo-id> "content"${RESET}
|
|
46251
46450
|
`);
|
|
46252
46451
|
process.exit(2);
|
|
46253
46452
|
}
|
|
@@ -46314,8 +46513,8 @@ Cancelled by user (Ctrl+C)
|
|
|
46314
46513
|
if (process.stderr.isTTY)
|
|
46315
46514
|
printLogo();
|
|
46316
46515
|
if (args.template) {
|
|
46317
|
-
if (!args["no-
|
|
46318
|
-
|
|
46516
|
+
if (!args["no-bridge"] && !args["no-watch"])
|
|
46517
|
+
ensureBridgeRunning(apiUrl, apiKey);
|
|
46319
46518
|
const templateId = args.template;
|
|
46320
46519
|
const inputValues = {};
|
|
46321
46520
|
for (const kv of args.input || []) {
|
|
@@ -46402,8 +46601,8 @@ ${"\u2500".repeat(40)}
|
|
|
46402
46601
|
`);
|
|
46403
46602
|
}
|
|
46404
46603
|
if (args.resume || args.continue) {
|
|
46405
|
-
if (!args["no-
|
|
46406
|
-
|
|
46604
|
+
if (!args["no-bridge"])
|
|
46605
|
+
ensureBridgeRunning(apiUrl, apiKey);
|
|
46407
46606
|
const todoId = args.resume || cfgScope.data.last_todo_id;
|
|
46408
46607
|
if (!todoId) {
|
|
46409
46608
|
process.stderr.write(`Error: No recent todo found
|
|
@@ -46440,14 +46639,22 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
|
|
|
46440
46639
|
let preMatchedAgent = null;
|
|
46441
46640
|
let agents = null;
|
|
46442
46641
|
if (args.agent) {
|
|
46443
|
-
|
|
46444
|
-
|
|
46445
|
-
|
|
46446
|
-
|
|
46642
|
+
agents = await api.listAgentSettings();
|
|
46643
|
+
const { match, ambiguous } = resolveAgentMatch(agents, args.agent);
|
|
46644
|
+
if (ambiguous) {
|
|
46645
|
+
process.stderr.write(`Error: Ambiguous agent '${args.agent}' \u2014 ${ambiguous.length} matches. Re-run with the exact id:
|
|
46646
|
+
`);
|
|
46647
|
+
for (const a of ambiguous)
|
|
46648
|
+
process.stderr.write(` ${getDisplayName(a)} ${DIM}${getItemId(a)}${RESET}
|
|
46649
|
+
`);
|
|
46650
|
+
process.exit(1);
|
|
46651
|
+
}
|
|
46652
|
+
if (!match) {
|
|
46447
46653
|
process.stderr.write(`Error: Agent '${args.agent}' not found
|
|
46448
46654
|
`);
|
|
46449
46655
|
process.exit(1);
|
|
46450
46656
|
}
|
|
46657
|
+
preMatchedAgent = match;
|
|
46451
46658
|
cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
|
|
46452
46659
|
} else {
|
|
46453
46660
|
const pathArg = args.path || ".";
|
|
@@ -46480,8 +46687,8 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
|
|
|
46480
46687
|
}
|
|
46481
46688
|
process.stderr.write(`${DIM}Tip: ${randomTip()}${RESET}
|
|
46482
46689
|
`);
|
|
46483
|
-
if (!args["no-
|
|
46484
|
-
|
|
46690
|
+
if (!args["no-bridge"] && !args["no-watch"])
|
|
46691
|
+
ensureBridgeRunning(apiUrl, apiKey);
|
|
46485
46692
|
let content;
|
|
46486
46693
|
if (positionals.length > 0) {
|
|
46487
46694
|
content = positionals.join(" ");
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@todoforai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"todoforai-cli": "dist/todoai.js",
|
|
7
|
+
"todoai": "dist/todoai.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/todoai.js"
|