@todoforai/cli 0.1.13 → 0.1.15
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 +15 -15
- package/dist/todoai.js +217 -74
- package/package.json +2 -1
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
|
|
|
@@ -10,11 +10,11 @@ bun install -g @todoforai/cli
|
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
13
|
-
Just run `
|
|
13
|
+
Just run `todoforai-cli` — on first use it opens a browser for **device login** and saves the API key to `~/.todoforai/credentials.json` (shared with the edge daemon).
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
todoforai-cli # prompts device login if no key found
|
|
17
|
+
todoforai-cli login # explicit login
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
API URL resolution: `--api-url` flag → `TODOFORAI_API_URL` env → `https://api.todofor.ai`.
|
|
@@ -25,7 +25,7 @@ Project, agent, and last-todo state are stored **per API URL** under `per_api_ur
|
|
|
25
25
|
|
|
26
26
|
## Edge daemon
|
|
27
27
|
|
|
28
|
-
The CLI talks to the backend over WebSocket; **shell execution, file I/O, and tool calls happen in the edge daemon** running locally. On every run `
|
|
28
|
+
The CLI talks to the backend over WebSocket; **shell execution, file I/O, and tool calls happen in the edge daemon** running locally. On every run `todoforai-cli` spawns a detached edge process if none is running (PID-locked at `~/.todoforai/edge-<hash>.lock`, logs at `~/.todoforai/edge.log`). It keeps running after the CLI exits, so long-running tasks survive `Ctrl+D`.
|
|
29
29
|
|
|
30
30
|
Disable with `--no-edge` if you manage the edge yourself (e.g. systemd, separate terminal).
|
|
31
31
|
|
|
@@ -34,18 +34,18 @@ Disable with `--no-edge` if you manage the edge yourself (e.g. systemd, separate
|
|
|
34
34
|
### Create a todo from a prompt
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
echo "content" |
|
|
40
|
-
|
|
37
|
+
todoforai-cli "Fix the login bug"
|
|
38
|
+
todoforai-cli -n "Quick task" # non-interactive (run and exit)
|
|
39
|
+
echo "content" | todoforai-cli # pipe from stdin
|
|
40
|
+
todoforai-cli --path /my/project "Fix bug" # explicit workspace
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### Start from a registry template
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
todoforai-cli --template alternativeto-listing # interactive input prompts
|
|
47
|
+
todoforai-cli --template f5bot-monitoring-setup --input "monitoring_details=My Brand" # with inputs
|
|
48
|
+
todoforai-cli --template f5bot-monitoring-setup --no-watch --json # create only
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
When inputs are missing, the CLI prompts interactively (unless `-n`).
|
|
@@ -53,7 +53,7 @@ When inputs are missing, the CLI prompts interactively (unless `-n`).
|
|
|
53
53
|
### Inspect a todo (read-only)
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
|
|
56
|
+
todoforai-cli --inspect <todo-id>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Prints the full chat log: messages, tool calls (type, status, path/cmd), results, and errors. No logo, no interactive mode.
|
|
@@ -61,8 +61,8 @@ Prints the full chat log: messages, tool calls (type, status, path/cmd), results
|
|
|
61
61
|
### Resume / continue
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
todoforai-cli -c # continue most recent todo
|
|
65
|
+
todoforai-cli --resume <todo-id> # resume specific todo
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## All Options
|
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,6 +43129,7 @@ var package_default = {
|
|
|
43107
43129
|
version: "0.1.3",
|
|
43108
43130
|
type: "module",
|
|
43109
43131
|
bin: {
|
|
43132
|
+
"todoforai-cli": "dist/todoai.js",
|
|
43110
43133
|
todoai: "dist/todoai.js"
|
|
43111
43134
|
},
|
|
43112
43135
|
files: ["dist/todoai.js"],
|
|
@@ -43146,6 +43169,7 @@ var TodoStatus;
|
|
|
43146
43169
|
TodoStatus2["POSTPONED"] = "POSTPONED";
|
|
43147
43170
|
TodoStatus2["REVIEW_REQUESTED"] = "REVIEW_REQUESTED";
|
|
43148
43171
|
TodoStatus2["RUNNING"] = "RUNNING";
|
|
43172
|
+
TodoStatus2["WAITING_FOR_USER"] = "WAITING_FOR_USER";
|
|
43149
43173
|
TodoStatus2["COMPACTING"] = "COMPACTING";
|
|
43150
43174
|
TodoStatus2["STOPPING"] = "STOPPING";
|
|
43151
43175
|
TodoStatus2["READY"] = "READY";
|
|
@@ -43450,6 +43474,15 @@ var SERVER_TO_FRONTENDS = {
|
|
|
43450
43474
|
},
|
|
43451
43475
|
functions: {}
|
|
43452
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
|
+
]);
|
|
43453
43486
|
// ../packages/shared-fbe/src/bashPatterns.ts
|
|
43454
43487
|
function splitShellCommands(input) {
|
|
43455
43488
|
const parts = [];
|
|
@@ -43939,24 +43972,24 @@ function getEnv(name) {
|
|
|
43939
43972
|
}
|
|
43940
43973
|
function printUsage() {
|
|
43941
43974
|
process.stderr.write(`
|
|
43942
|
-
|
|
43975
|
+
todoforai-cli \u2014 TODOforAI CLI (Bun)
|
|
43943
43976
|
|
|
43944
43977
|
Usage:
|
|
43945
|
-
|
|
43946
|
-
|
|
43947
|
-
|
|
43948
|
-
echo "content" |
|
|
43949
|
-
|
|
43950
|
-
|
|
43951
|
-
|
|
43952
|
-
|
|
43953
|
-
|
|
43954
|
-
|
|
43955
|
-
|
|
43956
|
-
|
|
43957
|
-
|
|
43958
|
-
|
|
43959
|
-
|
|
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
|
|
43960
43993
|
|
|
43961
43994
|
Options:
|
|
43962
43995
|
--path <dir> Workspace path (default: cwd)
|
|
@@ -43998,7 +44031,7 @@ var STATUS_HELP = {
|
|
|
43998
44031
|
};
|
|
43999
44032
|
function printStatusHelp() {
|
|
44000
44033
|
process.stderr.write(`
|
|
44001
|
-
|
|
44034
|
+
todoforai-cli status <todo-id> <STATUS>
|
|
44002
44035
|
|
|
44003
44036
|
Common statuses:
|
|
44004
44037
|
${Object.entries(STATUS_HELP).map(([s, d]) => ` ${s.padEnd(18)}${d}`).join(`
|
|
@@ -45026,6 +45059,28 @@ function getItemId(item) {
|
|
|
45026
45059
|
return item.project.id;
|
|
45027
45060
|
return item?.id || "";
|
|
45028
45061
|
}
|
|
45062
|
+
function resolveAgentMatch(agents, query) {
|
|
45063
|
+
const byId = agents.find((a) => getItemId(a) === query);
|
|
45064
|
+
if (byId)
|
|
45065
|
+
return { match: byId };
|
|
45066
|
+
const q = query.toLowerCase();
|
|
45067
|
+
const esc = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45068
|
+
const boundary = new RegExp(`\\b${esc}`);
|
|
45069
|
+
const name = (a) => getDisplayName(a).toLowerCase();
|
|
45070
|
+
const tiers = [
|
|
45071
|
+
agents.filter((a) => name(a) === q),
|
|
45072
|
+
agents.filter((a) => name(a).startsWith(q)),
|
|
45073
|
+
agents.filter((a) => boundary.test(name(a))),
|
|
45074
|
+
agents.filter((a) => name(a).includes(q))
|
|
45075
|
+
];
|
|
45076
|
+
for (const tier of tiers) {
|
|
45077
|
+
if (tier.length === 1)
|
|
45078
|
+
return { match: tier[0] };
|
|
45079
|
+
if (tier.length > 1)
|
|
45080
|
+
return { ambiguous: tier };
|
|
45081
|
+
}
|
|
45082
|
+
return {};
|
|
45083
|
+
}
|
|
45029
45084
|
function terminalLine(prompt) {
|
|
45030
45085
|
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
45031
45086
|
return new Promise((resolve3) => {
|
|
@@ -45757,12 +45812,12 @@ async function listAgentsCommand(api, opts) {
|
|
|
45757
45812
|
// src/agent-command.ts
|
|
45758
45813
|
function printAgentHelp() {
|
|
45759
45814
|
process.stderr.write(`
|
|
45760
|
-
|
|
45815
|
+
todoforai-cli agent \u2014 inspect and update agent settings
|
|
45761
45816
|
|
|
45762
45817
|
Usage:
|
|
45763
|
-
|
|
45764
|
-
|
|
45765
|
-
|
|
45818
|
+
todoforai-cli agent list List agents (name, model, id, paths)
|
|
45819
|
+
todoforai-cli agent get <agent> Show a single agent's settings
|
|
45820
|
+
todoforai-cli agent update <agent> <field=value>\u2026 Update one or more settings
|
|
45766
45821
|
|
|
45767
45822
|
<agent> is a name or id (unique partial name also works).
|
|
45768
45823
|
Fields map directly to agent settings; values are parsed as JSON when possible
|
|
@@ -45775,9 +45830,9 @@ Fields map directly to agent settings; values are parsed as JSON when possible
|
|
|
45775
45830
|
name agent display name
|
|
45776
45831
|
|
|
45777
45832
|
Examples:
|
|
45778
|
-
|
|
45779
|
-
|
|
45780
|
-
|
|
45833
|
+
todoforai-cli agent update <agent> model=claude
|
|
45834
|
+
todoforai-cli agent update <agent> model=anthropic:anthropic/claude-opus-4.8 temperature=0.5
|
|
45835
|
+
todoforai-cli agent update <agent> sysmsg="You are a terse video editor."
|
|
45781
45836
|
`);
|
|
45782
45837
|
}
|
|
45783
45838
|
var FIELD_ALIASES = {
|
|
@@ -45803,20 +45858,13 @@ function parseAssignment(arg) {
|
|
|
45803
45858
|
}
|
|
45804
45859
|
}
|
|
45805
45860
|
function resolveAgent(agents, query) {
|
|
45806
|
-
const
|
|
45807
|
-
if (
|
|
45808
|
-
return
|
|
45809
|
-
|
|
45810
|
-
|
|
45811
|
-
if (exact.length === 1)
|
|
45812
|
-
return exact[0];
|
|
45813
|
-
const partial = agents.filter((a) => getDisplayName(a).toLowerCase().includes(q));
|
|
45814
|
-
if (partial.length === 1)
|
|
45815
|
-
return partial[0];
|
|
45816
|
-
if (partial.length > 1) {
|
|
45817
|
-
process.stderr.write(`${RED}Ambiguous agent '${query}', matches:${RESET}
|
|
45861
|
+
const { match, ambiguous } = resolveAgentMatch(agents, query);
|
|
45862
|
+
if (match)
|
|
45863
|
+
return match;
|
|
45864
|
+
if (ambiguous) {
|
|
45865
|
+
process.stderr.write(`${RED}Ambiguous agent '${query}' \u2014 ${ambiguous.length} matches. Re-run with the exact id:${RESET}
|
|
45818
45866
|
`);
|
|
45819
|
-
for (const a of
|
|
45867
|
+
for (const a of ambiguous)
|
|
45820
45868
|
process.stderr.write(` ${getDisplayName(a)} ${DIM}${getItemId(a)}${RESET}
|
|
45821
45869
|
`);
|
|
45822
45870
|
process.exit(2);
|
|
@@ -45852,7 +45900,7 @@ async function agentCommand(api, positionals, args, formatPath) {
|
|
|
45852
45900
|
const query = positionals[2];
|
|
45853
45901
|
const assignments = positionals.slice(3);
|
|
45854
45902
|
if (!query || !assignments.length) {
|
|
45855
|
-
process.stderr.write(`${RED}Usage:
|
|
45903
|
+
process.stderr.write(`${RED}Usage: todoforai-cli agent update <name|id> <field=value>\u2026${RESET}
|
|
45856
45904
|
`);
|
|
45857
45905
|
process.exit(2);
|
|
45858
45906
|
}
|
|
@@ -45906,34 +45954,87 @@ var CLOSED = new Set([
|
|
|
45906
45954
|
]);
|
|
45907
45955
|
var VALID_STATUS = new Set(Object.values(TodoStatus));
|
|
45908
45956
|
var isOpen = (s) => !CLOSED.has(s);
|
|
45909
|
-
function
|
|
45957
|
+
function printListTodosHelp() {
|
|
45910
45958
|
process.stderr.write(`
|
|
45911
|
-
|
|
45959
|
+
todoforai-cli list \u2014 list todos in a project (recent first)
|
|
45912
45960
|
|
|
45913
45961
|
Usage:
|
|
45914
|
-
|
|
45962
|
+
todoforai-cli list [flags]
|
|
45915
45963
|
|
|
45916
45964
|
Flags:
|
|
45917
45965
|
-n, --limit <n> Max rows to show (default: 30)
|
|
45966
|
+
--cursor <n> Fetch todos older than this cursor (lastActivityAt)
|
|
45967
|
+
--page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
|
|
45968
|
+
--search <text> Search content/status on the backend
|
|
45918
45969
|
-s, --status <S[,S2]> Filter by status (comma-separated, union).
|
|
45919
45970
|
-A, --all Include DONE (also CANCELLED, ARCHIVED, \u2026)
|
|
45920
45971
|
--project <id> Project ID (default: current default project)
|
|
45921
|
-
--json Output
|
|
45972
|
+
--json Output { items, nextCursor } as JSON
|
|
45922
45973
|
-h, --help Show this help
|
|
45923
45974
|
|
|
45924
45975
|
Examples:
|
|
45925
|
-
|
|
45926
|
-
|
|
45927
|
-
|
|
45928
|
-
|
|
45929
|
-
|
|
45976
|
+
todoforai-cli list # 30 most recent open todos
|
|
45977
|
+
todoforai-cli list -n 50 # last 50 open
|
|
45978
|
+
todoforai-cli list --cursor 1719234567890 # next page
|
|
45979
|
+
todoforai-cli list --all # include DONE
|
|
45980
|
+
todoforai-cli list -s RUNNING,REVIEW_REQUESTED
|
|
45981
|
+
todoforai-cli list --search bug --json | jq '.items[].id'
|
|
45982
|
+
`);
|
|
45983
|
+
}
|
|
45984
|
+
function positiveInt(value, name) {
|
|
45985
|
+
if (value === undefined)
|
|
45986
|
+
return;
|
|
45987
|
+
const n = Number(value);
|
|
45988
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
45989
|
+
process.stderr.write(`${RED}Error: ${name} must be a positive integer${RESET}
|
|
45990
|
+
`);
|
|
45991
|
+
process.exit(2);
|
|
45992
|
+
}
|
|
45993
|
+
return n;
|
|
45994
|
+
}
|
|
45995
|
+
function cursorValue(value) {
|
|
45996
|
+
if (value === undefined)
|
|
45997
|
+
return;
|
|
45998
|
+
const n = Number(value);
|
|
45999
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
46000
|
+
process.stderr.write(`${RED}Error: --cursor must be a non-negative number${RESET}
|
|
45930
46001
|
`);
|
|
46002
|
+
process.exit(2);
|
|
46003
|
+
}
|
|
46004
|
+
return n;
|
|
46005
|
+
}
|
|
46006
|
+
function toItemsPage(response) {
|
|
46007
|
+
if (Array.isArray(response))
|
|
46008
|
+
return { items: response };
|
|
46009
|
+
return {
|
|
46010
|
+
items: Array.isArray(response?.items) ? response.items : [],
|
|
46011
|
+
nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
|
|
46012
|
+
};
|
|
46013
|
+
}
|
|
46014
|
+
function rowTime(t) {
|
|
46015
|
+
return Number(t.lastActivityAt ?? t.createdAt ?? 0);
|
|
46016
|
+
}
|
|
46017
|
+
function printStatusError(unknown) {
|
|
46018
|
+
process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
|
|
46019
|
+
`);
|
|
46020
|
+
process.stderr.write(`${DIM}Use OPEN or one of: ${Object.values(TodoStatus).join(", ")}${RESET}
|
|
46021
|
+
`);
|
|
46022
|
+
process.exit(2);
|
|
46023
|
+
}
|
|
46024
|
+
function matchesRequestedStatus(t, requested, wanted, wantOpen) {
|
|
46025
|
+
if (!requested)
|
|
46026
|
+
return true;
|
|
46027
|
+
const s = String(t.status).toUpperCase();
|
|
46028
|
+
return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
|
|
45931
46029
|
}
|
|
45932
46030
|
async function listTodosCommand(api, defaultProjectId, argv) {
|
|
45933
46031
|
const { values } = parseArgs2({
|
|
45934
46032
|
args: argv,
|
|
45935
46033
|
options: {
|
|
45936
46034
|
limit: { type: "string", short: "n" },
|
|
46035
|
+
cursor: { type: "string" },
|
|
46036
|
+
"page-size": { type: "string" },
|
|
46037
|
+
search: { type: "string" },
|
|
45937
46038
|
status: { type: "string", short: "s" },
|
|
45938
46039
|
all: { type: "boolean", short: "A", default: false },
|
|
45939
46040
|
project: { type: "string" },
|
|
@@ -45943,7 +46044,7 @@ async function listTodosCommand(api, defaultProjectId, argv) {
|
|
|
45943
46044
|
strict: true
|
|
45944
46045
|
});
|
|
45945
46046
|
if (values.help) {
|
|
45946
|
-
|
|
46047
|
+
printListTodosHelp();
|
|
45947
46048
|
return;
|
|
45948
46049
|
}
|
|
45949
46050
|
const projectId = values.project || defaultProjectId;
|
|
@@ -45952,38 +46053,68 @@ async function listTodosCommand(api, defaultProjectId, argv) {
|
|
|
45952
46053
|
`);
|
|
45953
46054
|
process.exit(1);
|
|
45954
46055
|
}
|
|
45955
|
-
const limit = values.limit
|
|
46056
|
+
const limit = positiveInt(values.limit, "--limit") ?? 30;
|
|
46057
|
+
const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
|
|
46058
|
+
let cursor = cursorValue(values.cursor);
|
|
46059
|
+
const search = values.search ? String(values.search) : undefined;
|
|
45956
46060
|
const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
|
|
45957
|
-
const
|
|
46061
|
+
const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
|
|
46062
|
+
if (unknown.length)
|
|
46063
|
+
printStatusError(unknown);
|
|
46064
|
+
const wantOpen = !!requested?.some((s) => s === "OPEN");
|
|
45958
46065
|
const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
|
|
45959
46066
|
const includeClosed = !!values.all || !!wanted && wanted.size > 0;
|
|
45960
|
-
const
|
|
45961
|
-
let
|
|
45962
|
-
|
|
45963
|
-
|
|
45964
|
-
const
|
|
45965
|
-
|
|
45966
|
-
|
|
46067
|
+
const rows = [];
|
|
46068
|
+
let backendNextCursor;
|
|
46069
|
+
while (rows.length < limit) {
|
|
46070
|
+
const requestLimit = Math.min(pageSize, limit - rows.length);
|
|
46071
|
+
const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
|
|
46072
|
+
backendNextCursor = page.nextCursor;
|
|
46073
|
+
for (const t of page.items) {
|
|
46074
|
+
const status = String(t.status).toUpperCase();
|
|
46075
|
+
if (!includeClosed && CLOSED.has(status))
|
|
46076
|
+
continue;
|
|
46077
|
+
if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
|
|
46078
|
+
continue;
|
|
46079
|
+
rows.push(t);
|
|
46080
|
+
if (rows.length >= limit)
|
|
46081
|
+
break;
|
|
46082
|
+
}
|
|
46083
|
+
if (!backendNextCursor || page.items.length === 0)
|
|
46084
|
+
break;
|
|
46085
|
+
cursor = backendNextCursor;
|
|
46086
|
+
}
|
|
46087
|
+
rows.sort((a, b) => rowTime(b) - rowTime(a));
|
|
46088
|
+
const visible = rows.slice(0, limit);
|
|
46089
|
+
const hasBufferedRows = rows.length > visible.length;
|
|
46090
|
+
const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
|
|
45967
46091
|
if (values.json) {
|
|
45968
|
-
process.stdout.write(JSON.stringify(
|
|
46092
|
+
process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
|
|
45969
46093
|
`);
|
|
45970
46094
|
return;
|
|
45971
46095
|
}
|
|
45972
|
-
if (!
|
|
46096
|
+
if (!visible.length) {
|
|
45973
46097
|
process.stderr.write(`${DIM}(no todos)${RESET}
|
|
46098
|
+
`);
|
|
46099
|
+
if (nextCursor !== undefined)
|
|
46100
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
45974
46101
|
`);
|
|
45975
46102
|
return;
|
|
45976
46103
|
}
|
|
45977
|
-
const statusW =
|
|
45978
|
-
for (const t of
|
|
45979
|
-
const
|
|
45980
|
-
const
|
|
46104
|
+
const statusW = visible.reduce((n, r) => Math.max(n, String(r.status).length), 0);
|
|
46105
|
+
for (const t of visible) {
|
|
46106
|
+
const status = String(t.status).toUpperCase();
|
|
46107
|
+
const sc = STATUS_COLOR[status] || DIM;
|
|
46108
|
+
const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
|
|
45981
46109
|
const title = String(t.content ?? "").split(`
|
|
45982
46110
|
`)[0].slice(0, 100);
|
|
45983
|
-
process.stdout.write(`${sc}${
|
|
46111
|
+
process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
|
|
45984
46112
|
`);
|
|
45985
46113
|
}
|
|
45986
|
-
process.stderr.write(`${DIM}${
|
|
46114
|
+
process.stderr.write(`${DIM}${visible.length} todo(s)${RESET}
|
|
46115
|
+
`);
|
|
46116
|
+
if (nextCursor !== undefined)
|
|
46117
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
45987
46118
|
`);
|
|
45988
46119
|
}
|
|
45989
46120
|
|
|
@@ -46138,6 +46269,10 @@ Cancelled by user (Ctrl+C)
|
|
|
46138
46269
|
printAgentHelp();
|
|
46139
46270
|
process.exit(0);
|
|
46140
46271
|
}
|
|
46272
|
+
if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
|
|
46273
|
+
printListTodosHelp();
|
|
46274
|
+
process.exit(0);
|
|
46275
|
+
}
|
|
46141
46276
|
if (args.help && !["list", "ls", "agent"].includes(positionals[0])) {
|
|
46142
46277
|
printUsage();
|
|
46143
46278
|
process.exit(0);
|
|
@@ -46233,7 +46368,7 @@ Cancelled by user (Ctrl+C)
|
|
|
46233
46368
|
if (positionals[0] === "delete") {
|
|
46234
46369
|
const todoId = positionals[1];
|
|
46235
46370
|
if (!todoId) {
|
|
46236
|
-
process.stderr.write(`${RED}Usage:
|
|
46371
|
+
process.stderr.write(`${RED}Usage: todoforai-cli delete <todo-id>${RESET}
|
|
46237
46372
|
`);
|
|
46238
46373
|
process.exit(2);
|
|
46239
46374
|
}
|
|
@@ -46246,7 +46381,7 @@ Cancelled by user (Ctrl+C)
|
|
|
46246
46381
|
const [, todoId, ...rest] = positionals;
|
|
46247
46382
|
const content2 = rest.join(" ") || await readStdin();
|
|
46248
46383
|
if (!todoId || !content2) {
|
|
46249
|
-
process.stderr.write(`${RED}Usage:
|
|
46384
|
+
process.stderr.write(`${RED}Usage: todoforai-cli addmessage <todo-id> "content"${RESET}
|
|
46250
46385
|
`);
|
|
46251
46386
|
process.exit(2);
|
|
46252
46387
|
}
|
|
@@ -46439,14 +46574,22 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
|
|
|
46439
46574
|
let preMatchedAgent = null;
|
|
46440
46575
|
let agents = null;
|
|
46441
46576
|
if (args.agent) {
|
|
46442
|
-
|
|
46443
|
-
|
|
46444
|
-
|
|
46445
|
-
|
|
46577
|
+
agents = await api.listAgentSettings();
|
|
46578
|
+
const { match, ambiguous } = resolveAgentMatch(agents, args.agent);
|
|
46579
|
+
if (ambiguous) {
|
|
46580
|
+
process.stderr.write(`Error: Ambiguous agent '${args.agent}' \u2014 ${ambiguous.length} matches. Re-run with the exact id:
|
|
46581
|
+
`);
|
|
46582
|
+
for (const a of ambiguous)
|
|
46583
|
+
process.stderr.write(` ${getDisplayName(a)} ${DIM}${getItemId(a)}${RESET}
|
|
46584
|
+
`);
|
|
46585
|
+
process.exit(1);
|
|
46586
|
+
}
|
|
46587
|
+
if (!match) {
|
|
46446
46588
|
process.stderr.write(`Error: Agent '${args.agent}' not found
|
|
46447
46589
|
`);
|
|
46448
46590
|
process.exit(1);
|
|
46449
46591
|
}
|
|
46592
|
+
preMatchedAgent = match;
|
|
46450
46593
|
cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
|
|
46451
46594
|
} else {
|
|
46452
46595
|
const pathArg = args.path || ".";
|