@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.
Files changed (3) hide show
  1. package/README.md +15 -15
  2. package/dist/todoai.js +217 -74
  3. package/package.json +2 -1
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,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
- todoai \u2014 TODOforAI CLI (Bun)
43975
+ todoforai-cli \u2014 TODOforAI CLI (Bun)
43943
43976
 
43944
43977
  Usage:
43945
- todoai login # Browser-based device auth
43946
- todoai "prompt text" # Prompt as argument
43947
- todoai -n "Quick task" # Non-interactive (run and exit)
43948
- echo "content" | todoai # Pipe from stdin
43949
- todoai --path /my/project "Fix bug" # Explicit workspace path
43950
- todoai -c ["prompt"] # Resume last todo (optional prompt sent on attach)
43951
- todoai --resume <todo-id> ["prompt"] # Resume specific todo (optional prompt sent on attach)
43952
- todoai --inspect <todo-id>[@<slice>] # Read chat log. <slice> = -3:, :1, 5:10, 7 (Python-style)
43953
- todoai --template <id> [--input k=v] # Start from a registry template
43954
- todoai --list-agents # List available agents and exit
43955
- todoai agent update <agent> model=<model> # Update agent settings (see 'agent --help')
43956
- todoai list [-n 30] [--all] [--status S] # List todos (open + recent first); see 'list --help'
43957
- todoai status <todo-id> <STATUS> # Update a todo's status (run 'status --help' for the full list)
43958
- todoai delete <todo-id> # Permanently delete a todo
43959
- 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
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
- todoai status <todo-id> <STATUS>
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
- todoai agent \u2014 inspect and update agent settings
45815
+ todoforai-cli agent \u2014 inspect and update agent settings
45761
45816
 
45762
45817
  Usage:
45763
- todoai agent list List agents (name, model, id, paths)
45764
- todoai agent get <agent> Show a single agent's settings
45765
- 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
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
- todoai agent update <agent> model=claude
45779
- todoai agent update <agent> model=anthropic:anthropic/claude-opus-4.8 temperature=0.5
45780
- 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."
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 byId = agents.find((a) => getItemId(a) === query);
45807
- if (byId)
45808
- return byId;
45809
- const q = query.toLowerCase();
45810
- const exact = agents.filter((a) => getDisplayName(a).toLowerCase() === q);
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 partial)
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: 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}
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 printHelp() {
45957
+ function printListTodosHelp() {
45910
45958
  process.stderr.write(`
45911
- todoai list \u2014 list todos in a project (recent first)
45959
+ todoforai-cli list \u2014 list todos in a project (recent first)
45912
45960
 
45913
45961
  Usage:
45914
- todoai list [flags]
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 raw JSON
45972
+ --json Output { items, nextCursor } as JSON
45922
45973
  -h, --help Show this help
45923
45974
 
45924
45975
  Examples:
45925
- todoai list # 30 most recent open todos
45926
- todoai list -n 50 # last 50 open
45927
- todoai list --all # include DONE
45928
- todoai list -s RUNNING,REVIEW_REQUESTED
45929
- 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}
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
- printHelp();
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 ? 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;
45956
46060
  const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
45957
- 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");
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 todos = await api.listTodos(projectId);
45961
- let rows = todos.filter((t) => includeClosed || !CLOSED.has(String(t.status).toUpperCase())).filter((t) => {
45962
- if (!requested)
45963
- return true;
45964
- const s = String(t.status).toUpperCase();
45965
- return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
45966
- }).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;
45967
46091
  if (values.json) {
45968
- process.stdout.write(JSON.stringify(rows, null, 2) + `
46092
+ process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
45969
46093
  `);
45970
46094
  return;
45971
46095
  }
45972
- if (!rows.length) {
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 = rows.reduce((n, r) => Math.max(n, String(r.status).length), 0);
45978
- for (const t of rows) {
45979
- const sc = STATUS_COLOR[t.status] || DIM;
45980
- 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", " ");
45981
46109
  const title = String(t.content ?? "").split(`
45982
46110
  `)[0].slice(0, 100);
45983
- 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}
45984
46112
  `);
45985
46113
  }
45986
- 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}
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: todoai delete <todo-id>${RESET}
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: todoai addmessage <todo-id> "content"${RESET}
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
- const matches = await api.listAgentSettings({ name: args.agent });
46443
- if (matches.length > 0) {
46444
- preMatchedAgent = matches[0];
46445
- } 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) {
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 || ".";
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "bin": {
6
+ "todoforai-cli": "dist/todoai.js",
6
7
  "todoai": "dist/todoai.js"
7
8
  },
8
9
  "files": [