@todoforai/cli 0.1.14 → 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.
Files changed (3) hide show
  1. package/README.md +15 -15
  2. package/dist/todoai.js +218 -76
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # todoai CLI
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 `todoai` — on first use it opens a browser for **device login** and saves the API key to `~/.todoforai/credentials.json` (shared with the edge daemon).
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
- todoai # prompts device login if no key found
17
- todoai login # explicit login
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 `todoai` 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`.
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
- todoai "Fix the login bug"
38
- todoai -n "Quick task" # non-interactive (run and exit)
39
- echo "content" | todoai # pipe from stdin
40
- todoai --path /my/project "Fix bug" # explicit workspace
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
- todoai --template alternativeto-listing # interactive input prompts
47
- todoai --template f5bot-monitoring-setup --input "monitoring_details=My Brand" # with inputs
48
- todoai --template f5bot-monitoring-setup --no-watch --json # create only
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
- todoai --inspect <todo-id>
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
- todoai -c # continue most recent todo
65
- todoai --resume <todo-id> # resume specific todo
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
- const endpoint = projectId ? `/api/v1/projects/${projectId}/todos` : "/api/v1/todos";
42795
- return this.request("GET", endpoint);
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
- todoai: "dist/todoai.js",
43111
- "todoforai-cli": "dist/todoai.js"
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
- todoai \u2014 TODOforAI CLI (Bun)
43975
+ todoforai-cli \u2014 TODOforAI CLI (Bun)
43944
43976
 
43945
43977
  Usage:
43946
- todoai login # Browser-based device auth
43947
- todoai "prompt text" # Prompt as argument
43948
- todoai -n "Quick task" # Non-interactive (run and exit)
43949
- echo "content" | todoai # Pipe from stdin
43950
- todoai --path /my/project "Fix bug" # Explicit workspace path
43951
- todoai -c ["prompt"] # Resume last todo (optional prompt sent on attach)
43952
- todoai --resume <todo-id> ["prompt"] # Resume specific todo (optional prompt sent on attach)
43953
- todoai --inspect <todo-id>[@<slice>] # Read chat log. <slice> = -3:, :1, 5:10, 7 (Python-style)
43954
- todoai --template <id> [--input k=v] # Start from a registry template
43955
- todoai --list-agents # List available agents and exit
43956
- todoai agent update <agent> model=<model> # Update agent settings (see 'agent --help')
43957
- todoai list [-n 30] [--all] [--status S] # List todos (open + recent first); see 'list --help'
43958
- todoai status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
43959
- todoai delete <todo-id> # Permanently delete a todo
43960
- todoai addmessage <todo-id> "text" # Add a message to an existing todo
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)
@@ -43999,7 +44031,7 @@ var STATUS_HELP = {
43999
44031
  };
44000
44032
  function printStatusHelp() {
44001
44033
  process.stderr.write(`
44002
- todoai status <todo-id> <STATUS>
44034
+ todoforai-cli status <todo-id> <STATUS>
44003
44035
 
44004
44036
  Common statuses:
44005
44037
  ${Object.entries(STATUS_HELP).map(([s, d]) => ` ${s.padEnd(18)}${d}`).join(`
@@ -45027,6 +45059,28 @@ function getItemId(item) {
45027
45059
  return item.project.id;
45028
45060
  return item?.id || "";
45029
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
+ }
45030
45084
  function terminalLine(prompt) {
45031
45085
  const rl = createInterface2({ input: process.stdin, output: process.stderr });
45032
45086
  return new Promise((resolve3) => {
@@ -45758,12 +45812,12 @@ async function listAgentsCommand(api, opts) {
45758
45812
  // src/agent-command.ts
45759
45813
  function printAgentHelp() {
45760
45814
  process.stderr.write(`
45761
- todoai agent \u2014 inspect and update agent settings
45815
+ todoforai-cli agent \u2014 inspect and update agent settings
45762
45816
 
45763
45817
  Usage:
45764
- todoai agent list List agents (name, model, id, paths)
45765
- todoai agent get <agent> Show a single agent's settings
45766
- todoai agent update <agent> <field=value>\u2026 Update one or more settings
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
45767
45821
 
45768
45822
  <agent> is a name or id (unique partial name also works).
45769
45823
  Fields map directly to agent settings; values are parsed as JSON when possible
@@ -45776,9 +45830,9 @@ Fields map directly to agent settings; values are parsed as JSON when possible
45776
45830
  name agent display name
45777
45831
 
45778
45832
  Examples:
45779
- todoai agent update <agent> model=claude
45780
- todoai agent update <agent> model=anthropic:anthropic/claude-opus-4.8 temperature=0.5
45781
- todoai agent update <agent> sysmsg="You are a terse video editor."
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."
45782
45836
  `);
45783
45837
  }
45784
45838
  var FIELD_ALIASES = {
@@ -45804,20 +45858,13 @@ function parseAssignment(arg) {
45804
45858
  }
45805
45859
  }
45806
45860
  function resolveAgent(agents, query) {
45807
- const byId = agents.find((a) => getItemId(a) === query);
45808
- if (byId)
45809
- return byId;
45810
- const q = query.toLowerCase();
45811
- const exact = agents.filter((a) => getDisplayName(a).toLowerCase() === q);
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}
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}
45819
45866
  `);
45820
- for (const a of partial)
45867
+ for (const a of ambiguous)
45821
45868
  process.stderr.write(` ${getDisplayName(a)} ${DIM}${getItemId(a)}${RESET}
45822
45869
  `);
45823
45870
  process.exit(2);
@@ -45853,7 +45900,7 @@ async function agentCommand(api, positionals, args, formatPath) {
45853
45900
  const query = positionals[2];
45854
45901
  const assignments = positionals.slice(3);
45855
45902
  if (!query || !assignments.length) {
45856
- process.stderr.write(`${RED}Usage: todoai agent update <name|id> <field=value>\u2026${RESET}
45903
+ process.stderr.write(`${RED}Usage: todoforai-cli agent update <name|id> <field=value>\u2026${RESET}
45857
45904
  `);
45858
45905
  process.exit(2);
45859
45906
  }
@@ -45907,34 +45954,87 @@ var CLOSED = new Set([
45907
45954
  ]);
45908
45955
  var VALID_STATUS = new Set(Object.values(TodoStatus));
45909
45956
  var isOpen = (s) => !CLOSED.has(s);
45910
- function printHelp() {
45957
+ function printListTodosHelp() {
45911
45958
  process.stderr.write(`
45912
- todoai list \u2014 list todos in a project (recent first)
45959
+ todoforai-cli list \u2014 list todos in a project (recent first)
45913
45960
 
45914
45961
  Usage:
45915
- todoai list [flags]
45962
+ todoforai-cli list [flags]
45916
45963
 
45917
45964
  Flags:
45918
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
45919
45969
  -s, --status <S[,S2]> Filter by status (comma-separated, union).
45920
45970
  -A, --all Include DONE (also CANCELLED, ARCHIVED, \u2026)
45921
45971
  --project <id> Project ID (default: current default project)
45922
- --json Output raw JSON
45972
+ --json Output { items, nextCursor } as JSON
45923
45973
  -h, --help Show this help
45924
45974
 
45925
45975
  Examples:
45926
- todoai list # 30 most recent open todos
45927
- todoai list -n 50 # last 50 open
45928
- todoai list --all # include DONE
45929
- todoai list -s RUNNING,REVIEW_REQUESTED
45930
- todoai list --json | jq '.[].id'
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}
45931
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);
45932
46029
  }
45933
46030
  async function listTodosCommand(api, defaultProjectId, argv) {
45934
46031
  const { values } = parseArgs2({
45935
46032
  args: argv,
45936
46033
  options: {
45937
46034
  limit: { type: "string", short: "n" },
46035
+ cursor: { type: "string" },
46036
+ "page-size": { type: "string" },
46037
+ search: { type: "string" },
45938
46038
  status: { type: "string", short: "s" },
45939
46039
  all: { type: "boolean", short: "A", default: false },
45940
46040
  project: { type: "string" },
@@ -45944,7 +46044,7 @@ async function listTodosCommand(api, defaultProjectId, argv) {
45944
46044
  strict: true
45945
46045
  });
45946
46046
  if (values.help) {
45947
- printHelp();
46047
+ printListTodosHelp();
45948
46048
  return;
45949
46049
  }
45950
46050
  const projectId = values.project || defaultProjectId;
@@ -45953,38 +46053,68 @@ async function listTodosCommand(api, defaultProjectId, argv) {
45953
46053
  `);
45954
46054
  process.exit(1);
45955
46055
  }
45956
- const limit = values.limit ? Math.max(1, Number(values.limit)) : 30;
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;
45957
46060
  const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
45958
- const wantOpen = !!requested?.some((s) => s === "OPEN" || !VALID_STATUS.has(s));
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");
45959
46065
  const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
45960
46066
  const includeClosed = !!values.all || !!wanted && wanted.size > 0;
45961
- const todos = await api.listTodos(projectId);
45962
- let rows = todos.filter((t) => includeClosed || !CLOSED.has(String(t.status).toUpperCase())).filter((t) => {
45963
- if (!requested)
45964
- return true;
45965
- const s = String(t.status).toUpperCase();
45966
- return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
45967
- }).sort((a, b) => (b.lastActivityAt ?? b.createdAt ?? 0) - (a.lastActivityAt ?? a.createdAt ?? 0)).slice(0, limit);
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;
45968
46091
  if (values.json) {
45969
- process.stdout.write(JSON.stringify(rows, null, 2) + `
46092
+ process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
45970
46093
  `);
45971
46094
  return;
45972
46095
  }
45973
- if (!rows.length) {
46096
+ if (!visible.length) {
45974
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}
45975
46101
  `);
45976
46102
  return;
45977
46103
  }
45978
- const statusW = rows.reduce((n, r) => Math.max(n, String(r.status).length), 0);
45979
- for (const t of rows) {
45980
- const sc = STATUS_COLOR[t.status] || DIM;
45981
- const ts = new Date(t.lastActivityAt ?? t.createdAt).toISOString().slice(0, 16).replace("T", " ");
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", " ");
45982
46109
  const title = String(t.content ?? "").split(`
45983
46110
  `)[0].slice(0, 100);
45984
- process.stdout.write(`${sc}${String(t.status).padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
46111
+ process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
45985
46112
  `);
45986
46113
  }
45987
- process.stderr.write(`${DIM}${rows.length} todo(s)${RESET}
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}
45988
46118
  `);
45989
46119
  }
45990
46120
 
@@ -46139,6 +46269,10 @@ Cancelled by user (Ctrl+C)
46139
46269
  printAgentHelp();
46140
46270
  process.exit(0);
46141
46271
  }
46272
+ if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
46273
+ printListTodosHelp();
46274
+ process.exit(0);
46275
+ }
46142
46276
  if (args.help && !["list", "ls", "agent"].includes(positionals[0])) {
46143
46277
  printUsage();
46144
46278
  process.exit(0);
@@ -46234,7 +46368,7 @@ Cancelled by user (Ctrl+C)
46234
46368
  if (positionals[0] === "delete") {
46235
46369
  const todoId = positionals[1];
46236
46370
  if (!todoId) {
46237
- process.stderr.write(`${RED}Usage: todoai delete <todo-id>${RESET}
46371
+ process.stderr.write(`${RED}Usage: todoforai-cli delete <todo-id>${RESET}
46238
46372
  `);
46239
46373
  process.exit(2);
46240
46374
  }
@@ -46247,7 +46381,7 @@ Cancelled by user (Ctrl+C)
46247
46381
  const [, todoId, ...rest] = positionals;
46248
46382
  const content2 = rest.join(" ") || await readStdin();
46249
46383
  if (!todoId || !content2) {
46250
- process.stderr.write(`${RED}Usage: todoai addmessage <todo-id> "content"${RESET}
46384
+ process.stderr.write(`${RED}Usage: todoforai-cli addmessage <todo-id> "content"${RESET}
46251
46385
  `);
46252
46386
  process.exit(2);
46253
46387
  }
@@ -46440,14 +46574,22 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46440
46574
  let preMatchedAgent = null;
46441
46575
  let agents = null;
46442
46576
  if (args.agent) {
46443
- const matches = await api.listAgentSettings({ name: args.agent });
46444
- if (matches.length > 0) {
46445
- preMatchedAgent = matches[0];
46446
- } else {
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) {
46447
46588
  process.stderr.write(`Error: Agent '${args.agent}' not found
46448
46589
  `);
46449
46590
  process.exit(1);
46450
46591
  }
46592
+ preMatchedAgent = match;
46451
46593
  cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
46452
46594
  } else {
46453
46595
  const pathArg = args.path || ".";
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "bin": {
6
- "todoai": "dist/todoai.js",
7
- "todoforai-cli": "dist/todoai.js"
6
+ "todoforai-cli": "dist/todoai.js",
7
+ "todoai": "dist/todoai.js"
8
8
  },
9
9
  "files": [
10
10
  "dist/todoai.js"