@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 +9 -6
- package/dist/todoai.js +84 -19
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
##
|
|
28
|
+
## Bridge
|
|
27
29
|
|
|
28
|
-
The CLI talks to the backend over WebSocket; **shell execution, file I/O, and tool calls happen in the
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
46127
|
-
const probe = spawnSync(
|
|
46130
|
+
function hasBridge() {
|
|
46131
|
+
const probe = spawnSync("todoforai-bridge", ["--version"], { stdio: "ignore" });
|
|
46128
46132
|
return probe.status === 0;
|
|
46129
46133
|
}
|
|
46130
|
-
function
|
|
46131
|
-
|
|
46132
|
-
|
|
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, "
|
|
46202
|
+
const logFile = path3.join(logDir, "bridge.log");
|
|
46138
46203
|
const out = fs2.openSync(logFile, "a");
|
|
46139
|
-
const child = spawn("
|
|
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
|
|
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
|
|
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[
|
|
46228
|
+
console.error(`\x1B[2mBridge exited cleanly. Logs: ${shortLog}\x1B[0m`);
|
|
46164
46229
|
} else {
|
|
46165
|
-
console.error(`\x1B[
|
|
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-
|
|
46452
|
-
|
|
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-
|
|
46540
|
-
|
|
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-
|
|
46626
|
-
|
|
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(" ");
|