@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.
Files changed (3) hide show
  1. package/README.md +22 -19
  2. package/dist/todoai.js +302 -95
  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
 
@@ -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 `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).
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
- todoai # prompts device login if no key found
17
- todoai login # explicit login
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 (`~/.todoforai/credentials.json`) → device login.
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
- ## Edge daemon
28
+ ## Bridge
27
29
 
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`.
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-edge` if you manage the edge yourself (e.g. systemd, separate terminal).
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
- 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
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
- 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
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
- todoai --inspect <todo-id>
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
- todoai -c # continue most recent todo
65
- todoai --resume <todo-id> # resume specific todo
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-edge Do not auto-spawn edge daemon
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
- 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)
@@ -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-edge Do not auto-spawn edge daemon
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
- todoai status <todo-id> <STATUS>
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
- todoai agent \u2014 inspect and update agent settings
45819
+ todoforai-cli agent \u2014 inspect and update agent settings
45762
45820
 
45763
45821
  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
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
- 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."
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 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}
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 partial)
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: todoai agent update <name|id> <field=value>\u2026${RESET}
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 printHelp() {
45961
+ function printListTodosHelp() {
45911
45962
  process.stderr.write(`
45912
- todoai list \u2014 list todos in a project (recent first)
45963
+ todoforai-cli list \u2014 list todos in a project (recent first)
45913
45964
 
45914
45965
  Usage:
45915
- todoai list [flags]
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 raw JSON
45976
+ --json Output { items, nextCursor } as JSON
45923
45977
  -h, --help Show this help
45924
45978
 
45925
45979
  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'
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
- printHelp();
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 ? Math.max(1, Number(values.limit)) : 30;
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 wantOpen = !!requested?.some((s) => s === "OPEN" || !VALID_STATUS.has(s));
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 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);
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(rows, null, 2) + `
46096
+ process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
45970
46097
  `);
45971
46098
  return;
45972
46099
  }
45973
- if (!rows.length) {
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 = 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", " ");
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}${String(t.status).padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
46115
+ process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
45985
46116
  `);
45986
46117
  }
45987
- process.stderr.write(`${DIM}${rows.length} todo(s)${RESET}
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-edge.ts
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 hasBunx() {
45997
- const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bunx"], { stdio: "ignore" });
46130
+ function hasBridge() {
46131
+ const probe = spawnSync("todoforai-bridge", ["--version"], { stdio: "ignore" });
45998
46132
  return probe.status === 0;
45999
46133
  }
46000
- function ensureEdgeRunning(apiUrl, apiKey) {
46001
- if (!hasBunx()) {
46002
- console.error("\x1B[2mEdge daemon not started: `bunx` is missing. Install Bun from https://bun.sh to enable it, or pass --no-edge to silence this.\x1B[0m");
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, "edge.log");
46202
+ const logFile = path3.join(logDir, "bridge.log");
46008
46203
  const out = fs2.openSync(logFile, "a");
46009
- const child = spawn("bunx", ["@todoforai/edge", "--api-url", apiUrl, "--api-key", apiKey], {
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 edge daemon: ${err.message}\x1B[0m`);
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 edge daemon (pid ${pid}), logs: ${shortLog}\x1B[0m`);
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[2mEdge daemon exited cleanly (another instance likely already running). Logs: ${shortLog}\x1B[0m`);
46228
+ console.error(`\x1B[2mBridge exited cleanly. Logs: ${shortLog}\x1B[0m`);
46034
46229
  } else {
46035
- console.error(`\x1B[31mEdge daemon died (exit ${exitCode}). Check logs: ${shortLog}\x1B[0m`);
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: todoai delete <todo-id>${RESET}
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: todoai addmessage <todo-id> "content"${RESET}
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-edge"] && !args["no-watch"])
46318
- ensureEdgeRunning(apiUrl, apiKey);
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-edge"])
46406
- ensureEdgeRunning(apiUrl, apiKey);
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
- const matches = await api.listAgentSettings({ name: args.agent });
46444
- if (matches.length > 0) {
46445
- preMatchedAgent = matches[0];
46446
- } else {
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-edge"] && !args["no-watch"])
46484
- ensureEdgeRunning(apiUrl, apiKey);
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.14",
3
+ "version": "0.1.16",
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"