@todoforai/cli 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,11 +6,13 @@ 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 `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).
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
18
  todoforai-cli # prompts device login if no key found
@@ -19,15 +21,15 @@ todoforai-cli login # explicit login
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 `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`.
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
 
@@ -82,7 +84,8 @@ todoforai-cli --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
@@ -44008,7 +44008,8 @@ Options:
44008
44008
  --allow-all Set permissions to allow all tools (no approval needed)
44009
44009
  --raw-sysmsg <file> Use file contents verbatim as system prompt (new TODO only)
44010
44010
  --no-watch Create todo and exit
44011
- --no-edge Do not auto-spawn edge daemon
44011
+ --no-bridge Do not auto-spawn bridge
44012
+ --no-edge Deprecated alias for --no-bridge
44012
44013
  --json Output as JSON
44013
44014
  --detailed 'inspect --json': keep ids, timestamps, agentSettingsId, scheduledTimestamp
44014
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
@@ -44062,6 +44063,7 @@ function parseCliArgs() {
44062
44063
  "allow-all": { type: "boolean", default: false },
44063
44064
  "raw-sysmsg": { type: "string" },
44064
44065
  "no-watch": { type: "boolean", default: false },
44066
+ "no-bridge": { type: "boolean", default: false },
44065
44067
  "no-edge": { type: "boolean", default: false },
44066
44068
  json: { type: "boolean", default: false },
44067
44069
  detailed: { type: "boolean", default: false },
@@ -44077,6 +44079,8 @@ function parseCliArgs() {
44077
44079
  allowPositionals: true,
44078
44080
  strict: false
44079
44081
  });
44082
+ if (values["no-edge"])
44083
+ values["no-bridge"] = true;
44080
44084
  return { values, positionals };
44081
44085
  }
44082
44086
 
@@ -46118,30 +46122,91 @@ async function listTodosCommand(api, defaultProjectId, argv) {
46118
46122
  `);
46119
46123
  }
46120
46124
 
46121
- // src/ensure-edge.ts
46125
+ // src/ensure-bridge.ts
46122
46126
  import { spawn, spawnSync } from "child_process";
46123
46127
  import fs2 from "fs";
46124
46128
  import path3 from "path";
46125
46129
  import os3 from "os";
46126
- function hasBunx() {
46127
- const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bunx"], { stdio: "ignore" });
46130
+ function hasBridge() {
46131
+ const probe = spawnSync("todoforai-bridge", ["--version"], { stdio: "ignore" });
46128
46132
  return probe.status === 0;
46129
46133
  }
46130
- function ensureEdgeRunning(apiUrl, apiKey) {
46131
- if (!hasBunx()) {
46132
- 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");
46133
46198
  return;
46134
46199
  }
46135
46200
  const logDir = path3.join(os3.homedir(), ".todoforai");
46136
46201
  fs2.mkdirSync(logDir, { recursive: true });
46137
- const logFile = path3.join(logDir, "edge.log");
46202
+ const logFile = path3.join(logDir, "bridge.log");
46138
46203
  const out = fs2.openSync(logFile, "a");
46139
- const child = spawn("bunx", ["@todoforai/edge", "--api-url", apiUrl, "--api-key", apiKey], {
46204
+ const child = spawn("todoforai-bridge", bridgeRunArgs(apiUrl), {
46140
46205
  detached: true,
46141
46206
  stdio: ["ignore", out, out]
46142
46207
  });
46143
46208
  child.on("error", (err) => {
46144
- console.error(`\x1B[33mFailed to start edge daemon: ${err.message}\x1B[0m`);
46209
+ console.error(`\x1B[33mFailed to start bridge: ${err.message}\x1B[0m`);
46145
46210
  });
46146
46211
  let exited = false;
46147
46212
  let exitCode = null;
@@ -46156,13 +46221,13 @@ function ensureEdgeRunning(apiUrl, apiKey) {
46156
46221
  const shortLog = logFile.replace(os3.homedir(), "~");
46157
46222
  setTimeout(() => {
46158
46223
  if (!exited) {
46159
- console.error(`\x1B[2mStarted edge daemon (pid ${pid}), logs: ${shortLog}\x1B[0m`);
46224
+ console.error(`\x1B[2mStarted bridge (pid ${pid}), logs: ${shortLog}\x1B[0m`);
46160
46225
  return;
46161
46226
  }
46162
46227
  if (exitCode === 0) {
46163
- 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`);
46164
46229
  } else {
46165
- 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`);
46166
46231
  }
46167
46232
  }, 500);
46168
46233
  }
@@ -46448,8 +46513,8 @@ Cancelled by user (Ctrl+C)
46448
46513
  if (process.stderr.isTTY)
46449
46514
  printLogo();
46450
46515
  if (args.template) {
46451
- if (!args["no-edge"] && !args["no-watch"])
46452
- ensureEdgeRunning(apiUrl, apiKey);
46516
+ if (!args["no-bridge"] && !args["no-watch"])
46517
+ ensureBridgeRunning(apiUrl, apiKey);
46453
46518
  const templateId = args.template;
46454
46519
  const inputValues = {};
46455
46520
  for (const kv of args.input || []) {
@@ -46536,8 +46601,8 @@ ${"\u2500".repeat(40)}
46536
46601
  `);
46537
46602
  }
46538
46603
  if (args.resume || args.continue) {
46539
- if (!args["no-edge"])
46540
- ensureEdgeRunning(apiUrl, apiKey);
46604
+ if (!args["no-bridge"])
46605
+ ensureBridgeRunning(apiUrl, apiKey);
46541
46606
  const todoId = args.resume || cfgScope.data.last_todo_id;
46542
46607
  if (!todoId) {
46543
46608
  process.stderr.write(`Error: No recent todo found
@@ -46622,8 +46687,8 @@ Resumed: ${CYAN}${getFrontendUrl(apiUrl, projectId2, todoId)}${RESET}
46622
46687
  }
46623
46688
  process.stderr.write(`${DIM}Tip: ${randomTip()}${RESET}
46624
46689
  `);
46625
- if (!args["no-edge"] && !args["no-watch"])
46626
- ensureEdgeRunning(apiUrl, apiKey);
46690
+ if (!args["no-bridge"] && !args["no-watch"])
46691
+ ensureBridgeRunning(apiUrl, apiKey);
46627
46692
  let content;
46628
46693
  if (positionals.length > 0) {
46629
46694
  content = positionals.join(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "dist/todoai.js",