@todoforai/cli 0.1.3 → 0.1.4
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 +18 -3
- package/dist/todoai.js +183 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,11 +10,27 @@ 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).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
todoai # prompts device login if no key found
|
|
17
|
+
todoai login # explicit login
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optional manual config:
|
|
21
|
+
|
|
13
22
|
```bash
|
|
14
23
|
todoai --set-default-api-url http://localhost:4000 # or https://api.todofor.ai
|
|
15
|
-
todoai --set-default-api-key <your-api-key>
|
|
16
24
|
```
|
|
17
25
|
|
|
26
|
+
Auth resolution order: `--api-key` flag → `TODOFORAI_API_KEY` env → shared credentials (`~/.todoforai/credentials.json`) → device login.
|
|
27
|
+
|
|
28
|
+
## Edge daemon
|
|
29
|
+
|
|
30
|
+
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`.
|
|
31
|
+
|
|
32
|
+
Disable with `--no-edge` if you manage the edge yourself (e.g. systemd, separate terminal).
|
|
33
|
+
|
|
18
34
|
## Usage
|
|
19
35
|
|
|
20
36
|
### Create a todo from a prompt
|
|
@@ -68,13 +84,12 @@ todoai --resume <todo-id> # resume specific todo
|
|
|
68
84
|
--dangerously-skip-permissions Auto-approve all blocks (CI/benchmarks)
|
|
69
85
|
--allow-all Set permissions to allow all tools (no approval needed)
|
|
70
86
|
--no-watch Create todo and exit
|
|
87
|
+
--no-edge Do not auto-spawn edge daemon
|
|
71
88
|
--json Output as JSON
|
|
72
89
|
--safe Validate API key upfront
|
|
73
90
|
--debug, -d Debug output
|
|
74
91
|
--show-config Show config
|
|
75
|
-
--set-defaults Interactive defaults setup
|
|
76
92
|
--set-default-api-url Set default API URL
|
|
77
|
-
--set-default-api-key Set default API key
|
|
78
93
|
--reset-config Reset config file
|
|
79
94
|
--help, -h Show this help
|
|
80
95
|
```
|
package/dist/todoai.js
CHANGED
|
@@ -1617,7 +1617,7 @@ var require_core = __commonJS((exports, module) => {
|
|
|
1617
1617
|
return match && match.index === 0;
|
|
1618
1618
|
}
|
|
1619
1619
|
var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
|
|
1620
|
-
function
|
|
1620
|
+
function join3(regexps, separator = "|") {
|
|
1621
1621
|
let numCaptures = 0;
|
|
1622
1622
|
return regexps.map((regex) => {
|
|
1623
1623
|
numCaptures += 1;
|
|
@@ -1900,7 +1900,7 @@ var require_core = __commonJS((exports, module) => {
|
|
|
1900
1900
|
this.exec = () => null;
|
|
1901
1901
|
}
|
|
1902
1902
|
const terminators = this.regexes.map((el) => el[1]);
|
|
1903
|
-
this.matcherRe = langRe(
|
|
1903
|
+
this.matcherRe = langRe(join3(terminators), true);
|
|
1904
1904
|
this.lastIndex = 0;
|
|
1905
1905
|
}
|
|
1906
1906
|
exec(s) {
|
|
@@ -42663,7 +42663,7 @@ var require_dist = __commonJS((exports) => {
|
|
|
42663
42663
|
// src/index.ts
|
|
42664
42664
|
import { realpathSync } from "fs";
|
|
42665
42665
|
import { resolve as resolve3 } from "path";
|
|
42666
|
-
import { homedir as
|
|
42666
|
+
import { homedir as homedir3 } from "os";
|
|
42667
42667
|
|
|
42668
42668
|
// src/tips.ts
|
|
42669
42669
|
var TIPS = [
|
|
@@ -42833,6 +42833,7 @@ function normalizeApiUrl(url) {
|
|
|
42833
42833
|
return `https://${url}`;
|
|
42834
42834
|
return url;
|
|
42835
42835
|
}
|
|
42836
|
+
var SUBCOMMANDS = new Set(["login", "logout"]);
|
|
42836
42837
|
var CREDENTIALS_PATH = path.join(os.homedir(), ".todoforai", "credentials.json");
|
|
42837
42838
|
|
|
42838
42839
|
// ../edge/bun/src/frontend-ws.ts
|
|
@@ -43045,11 +43046,13 @@ Usage:
|
|
|
43045
43046
|
todoai --resume <todo-id> # Resume specific todo
|
|
43046
43047
|
todoai --inspect <todo-id> # Print full chat log (read-only)
|
|
43047
43048
|
todoai --template <id> [--input k=v] # Start from a registry template
|
|
43049
|
+
todoai --list-agents # List available agents and exit
|
|
43048
43050
|
|
|
43049
43051
|
Options:
|
|
43050
43052
|
--path <dir> Workspace path (default: cwd)
|
|
43051
43053
|
--project <id> Project ID
|
|
43052
43054
|
--agent, -a <name> Agent name (partial match)
|
|
43055
|
+
--list-agents List available agents (name, id, workspace paths) and exit
|
|
43053
43056
|
--api-url <url> API URL
|
|
43054
43057
|
--api-key <key> API key
|
|
43055
43058
|
--inspect, -i <todo-id> Print full chat log (read-only, no interactive)
|
|
@@ -43061,13 +43064,12 @@ Options:
|
|
|
43061
43064
|
--dangerously-skip-permissions Auto-approve all blocks (for CI/benchmarks)
|
|
43062
43065
|
--allow-all Set permissions to allow all tools (no approval needed)
|
|
43063
43066
|
--no-watch Create todo and exit
|
|
43067
|
+
--no-edge Do not auto-spawn edge daemon
|
|
43064
43068
|
--json Output as JSON
|
|
43065
43069
|
--safe Validate API key upfront
|
|
43066
43070
|
--debug, -d Debug output
|
|
43067
43071
|
--show-config Show config
|
|
43068
|
-
--set-defaults Interactive defaults setup
|
|
43069
43072
|
--set-default-api-url Set default API URL
|
|
43070
|
-
--set-default-api-key Set default API key
|
|
43071
43073
|
--reset-config Reset config file
|
|
43072
43074
|
--help, -h Show this help
|
|
43073
43075
|
`);
|
|
@@ -43079,6 +43081,7 @@ function parseCliArgs() {
|
|
|
43079
43081
|
path: { type: "string", default: "." },
|
|
43080
43082
|
project: { type: "string" },
|
|
43081
43083
|
agent: { type: "string", short: "a" },
|
|
43084
|
+
"list-agents": { type: "boolean", default: false },
|
|
43082
43085
|
"api-url": { type: "string" },
|
|
43083
43086
|
"api-key": { type: "string" },
|
|
43084
43087
|
inspect: { type: "string", short: "i" },
|
|
@@ -43090,13 +43093,12 @@ function parseCliArgs() {
|
|
|
43090
43093
|
"dangerously-skip-permissions": { type: "boolean", default: false },
|
|
43091
43094
|
"allow-all": { type: "boolean", default: false },
|
|
43092
43095
|
"no-watch": { type: "boolean", default: false },
|
|
43096
|
+
"no-edge": { type: "boolean", default: false },
|
|
43093
43097
|
json: { type: "boolean", default: false },
|
|
43094
43098
|
safe: { type: "boolean", default: false },
|
|
43095
43099
|
debug: { type: "boolean", short: "d", default: false },
|
|
43096
43100
|
"show-config": { type: "boolean", default: false },
|
|
43097
|
-
"set-defaults": { type: "boolean", default: false },
|
|
43098
43101
|
"set-default-api-url": { type: "string" },
|
|
43099
|
-
"set-default-api-key": { type: "string" },
|
|
43100
43102
|
"reset-config": { type: "boolean", default: false },
|
|
43101
43103
|
"config-path": { type: "string" },
|
|
43102
43104
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -43493,18 +43495,6 @@ function getConfigDir() {
|
|
|
43493
43495
|
const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
43494
43496
|
return join(xdg, "todoai-cli");
|
|
43495
43497
|
}
|
|
43496
|
-
function obfuscate(s) {
|
|
43497
|
-
return s ? Buffer.from(s, "utf-8").toString("base64") : s;
|
|
43498
|
-
}
|
|
43499
|
-
function deobfuscate(s) {
|
|
43500
|
-
if (!s)
|
|
43501
|
-
return s;
|
|
43502
|
-
try {
|
|
43503
|
-
return Buffer.from(s, "base64").toString("utf-8");
|
|
43504
|
-
} catch {
|
|
43505
|
-
return s;
|
|
43506
|
-
}
|
|
43507
|
-
}
|
|
43508
43498
|
function defaultConfig() {
|
|
43509
43499
|
return {
|
|
43510
43500
|
default_project_id: null,
|
|
@@ -43513,7 +43503,6 @@ function defaultConfig() {
|
|
|
43513
43503
|
default_agent_settings: null,
|
|
43514
43504
|
default_agent_settings_updated_at: null,
|
|
43515
43505
|
default_api_url: null,
|
|
43516
|
-
default_api_key: null,
|
|
43517
43506
|
recent_projects: [],
|
|
43518
43507
|
recent_agents: [],
|
|
43519
43508
|
last_todo_id: null,
|
|
@@ -43538,8 +43527,7 @@ class ConfigStore {
|
|
|
43538
43527
|
return defaultConfig();
|
|
43539
43528
|
try {
|
|
43540
43529
|
const raw = JSON.parse(readFileSync(this.path, "utf-8"));
|
|
43541
|
-
|
|
43542
|
-
raw.default_api_key = deobfuscate(raw.default_api_key);
|
|
43530
|
+
delete raw.default_api_key;
|
|
43543
43531
|
return { ...defaultConfig(), ...raw };
|
|
43544
43532
|
} catch {
|
|
43545
43533
|
return defaultConfig();
|
|
@@ -43548,10 +43536,7 @@ class ConfigStore {
|
|
|
43548
43536
|
save() {
|
|
43549
43537
|
try {
|
|
43550
43538
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
43551
|
-
|
|
43552
|
-
if (out.default_api_key)
|
|
43553
|
-
out.default_api_key = obfuscate(out.default_api_key);
|
|
43554
|
-
writeFileSync(this.path, JSON.stringify(out, null, 2), "utf-8");
|
|
43539
|
+
writeFileSync(this.path, JSON.stringify(this.data, null, 2), "utf-8");
|
|
43555
43540
|
} catch {}
|
|
43556
43541
|
}
|
|
43557
43542
|
setDefaultProject(id, name) {
|
|
@@ -43575,10 +43560,6 @@ class ConfigStore {
|
|
|
43575
43560
|
this.data.default_api_url = url;
|
|
43576
43561
|
this.save();
|
|
43577
43562
|
}
|
|
43578
|
-
setDefaultApiKey(key) {
|
|
43579
|
-
this.data.default_api_key = key;
|
|
43580
|
-
this.save();
|
|
43581
|
-
}
|
|
43582
43563
|
addToHistory(input) {
|
|
43583
43564
|
const trimmed = input.trim();
|
|
43584
43565
|
if (!trimmed)
|
|
@@ -43595,6 +43576,30 @@ class ConfigStore {
|
|
|
43595
43576
|
}
|
|
43596
43577
|
}
|
|
43597
43578
|
|
|
43579
|
+
// src/credentials.ts
|
|
43580
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
43581
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
43582
|
+
import { homedir as homedir2 } from "os";
|
|
43583
|
+
var CREDENTIALS_PATH2 = join2(homedir2(), ".todoforai", "credentials.json");
|
|
43584
|
+
function read() {
|
|
43585
|
+
if (!existsSync2(CREDENTIALS_PATH2))
|
|
43586
|
+
return {};
|
|
43587
|
+
try {
|
|
43588
|
+
return JSON.parse(readFileSync2(CREDENTIALS_PATH2, "utf-8"));
|
|
43589
|
+
} catch {
|
|
43590
|
+
return {};
|
|
43591
|
+
}
|
|
43592
|
+
}
|
|
43593
|
+
function readCredential(apiUrl) {
|
|
43594
|
+
return read()[apiUrl] || "";
|
|
43595
|
+
}
|
|
43596
|
+
function writeCredential(apiUrl, apiKey) {
|
|
43597
|
+
const data = read();
|
|
43598
|
+
data[apiUrl] = apiKey;
|
|
43599
|
+
mkdirSync2(dirname2(CREDENTIALS_PATH2), { recursive: true });
|
|
43600
|
+
writeFileSync2(CREDENTIALS_PATH2, JSON.stringify(data, null, 2), "utf-8");
|
|
43601
|
+
}
|
|
43602
|
+
|
|
43598
43603
|
// src/logo.ts
|
|
43599
43604
|
var LETTERS = {
|
|
43600
43605
|
t: [" x ", "xxxx", " x ", " xll", " xll", " xxx"],
|
|
@@ -43913,6 +43918,12 @@ function splitShellCommands(input) {
|
|
|
43913
43918
|
const len = input.length;
|
|
43914
43919
|
while (i < len) {
|
|
43915
43920
|
const ch = input[i];
|
|
43921
|
+
if (ch === "\\" && i + 1 < len && input[i + 1] === `
|
|
43922
|
+
`) {
|
|
43923
|
+
current += " ";
|
|
43924
|
+
i += 2;
|
|
43925
|
+
continue;
|
|
43926
|
+
}
|
|
43916
43927
|
if (ch === "\\" && i + 1 < len) {
|
|
43917
43928
|
current += ch + input[i + 1];
|
|
43918
43929
|
i += 2;
|
|
@@ -44025,6 +44036,11 @@ function splitTokens(cmd) {
|
|
|
44025
44036
|
const len = cmd.length;
|
|
44026
44037
|
while (i < len) {
|
|
44027
44038
|
const ch = cmd[i];
|
|
44039
|
+
if (ch === "\\" && i + 1 < len && cmd[i + 1] === `
|
|
44040
|
+
`) {
|
|
44041
|
+
i += 2;
|
|
44042
|
+
continue;
|
|
44043
|
+
}
|
|
44028
44044
|
if (ch === "\\" && i + 1 < len) {
|
|
44029
44045
|
current += ch + cmd[i + 1];
|
|
44030
44046
|
i += 2;
|
|
@@ -44115,29 +44131,40 @@ function parseBashCmd(fullCmd) {
|
|
|
44115
44131
|
function generalizeBashCmd(fullCmd) {
|
|
44116
44132
|
return parseBashCmd(fullCmd).map((p) => buildBashPattern(p));
|
|
44117
44133
|
}
|
|
44134
|
+
function getBlockServerPrefix(block) {
|
|
44135
|
+
const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
|
|
44136
|
+
const prefixMatch = agentPattern.match(/^(.*?:)BASH(?:\(|$)/);
|
|
44137
|
+
return prefixMatch ? prefixMatch[1] : "";
|
|
44138
|
+
}
|
|
44139
|
+
var BASH_LIKE_BLOCK_TYPES = new Set(["bash", "machine_exec"]);
|
|
44118
44140
|
function getBlockPatterns(block) {
|
|
44119
44141
|
if (Array.isArray(block.generalized_pattern))
|
|
44120
44142
|
return block.generalized_pattern;
|
|
44121
|
-
|
|
44122
|
-
|
|
44123
|
-
|
|
44124
|
-
const prefix =
|
|
44143
|
+
const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
|
|
44144
|
+
const isBashLike = BASH_LIKE_BLOCK_TYPES.has(block.type.toLowerCase()) || /:BASH(\(|$)/.test(agentPattern);
|
|
44145
|
+
if (isBashLike && block.cmd) {
|
|
44146
|
+
const prefix = getBlockServerPrefix(block);
|
|
44125
44147
|
return generalizeBashCmd(block.cmd).map((p) => prefix + p);
|
|
44126
44148
|
}
|
|
44127
44149
|
if (block.generalized_pattern)
|
|
44128
44150
|
return [block.generalized_pattern];
|
|
44129
44151
|
return [`${block.type}(*)`];
|
|
44130
44152
|
}
|
|
44153
|
+
function getBlockServerId(block) {
|
|
44154
|
+
const prefix = getBlockServerPrefix(block);
|
|
44155
|
+
return prefix ? prefix.slice(0, -1) : "*";
|
|
44156
|
+
}
|
|
44131
44157
|
|
|
44132
44158
|
// ../packages/shared-fbe/src/permissionUtils.ts
|
|
44133
44159
|
function parsePattern(pattern) {
|
|
44134
44160
|
const colonIndex = pattern.indexOf(":");
|
|
44135
44161
|
if (colonIndex === -1)
|
|
44136
44162
|
return null;
|
|
44137
|
-
|
|
44138
|
-
|
|
44139
|
-
toolName: pattern
|
|
44140
|
-
}
|
|
44163
|
+
const serverId = pattern.slice(0, colonIndex);
|
|
44164
|
+
if (!/^[A-Za-z0-9_*-]+$/.test(serverId)) {
|
|
44165
|
+
return { serverId: "*", toolName: pattern };
|
|
44166
|
+
}
|
|
44167
|
+
return { serverId, toolName: pattern.slice(colonIndex + 1) };
|
|
44141
44168
|
}
|
|
44142
44169
|
function serverIdMatches(ruleServerId, targetServerId) {
|
|
44143
44170
|
if (ruleServerId === targetServerId)
|
|
@@ -44170,6 +44197,26 @@ function patternMatches(rulePattern, targetPattern) {
|
|
|
44170
44197
|
}
|
|
44171
44198
|
return false;
|
|
44172
44199
|
}
|
|
44200
|
+
function bashRuleMatchesCmd(rulePattern, serverId, cmd) {
|
|
44201
|
+
const rule = parsePattern(rulePattern);
|
|
44202
|
+
if (!rule)
|
|
44203
|
+
return false;
|
|
44204
|
+
if (!serverIdMatches(rule.serverId, serverId))
|
|
44205
|
+
return false;
|
|
44206
|
+
if (rule.toolName === "*" || rule.toolName === "BASH")
|
|
44207
|
+
return true;
|
|
44208
|
+
const m = rule.toolName.match(/^BASH\(cmd:\s*(.*)\)$/);
|
|
44209
|
+
if (!m)
|
|
44210
|
+
return false;
|
|
44211
|
+
let body = m[1];
|
|
44212
|
+
const isPrefix = body.endsWith(" *") || body.endsWith("*");
|
|
44213
|
+
if (isPrefix)
|
|
44214
|
+
body = body.replace(/\s*\*$/, "");
|
|
44215
|
+
const subs = splitShellCommands(cmd);
|
|
44216
|
+
if (subs.length === 0)
|
|
44217
|
+
return false;
|
|
44218
|
+
return subs.every((sub) => isPrefix ? sub === body || sub.startsWith(body + " ") : sub === body);
|
|
44219
|
+
}
|
|
44173
44220
|
function isPatternInList(list, pattern) {
|
|
44174
44221
|
if (!list)
|
|
44175
44222
|
return false;
|
|
@@ -44181,6 +44228,28 @@ function isPatternAllowed(permissions, pattern) {
|
|
|
44181
44228
|
function getNewPatterns(patterns, permissions) {
|
|
44182
44229
|
return patterns.filter((p) => !isPatternAllowed(permissions, p));
|
|
44183
44230
|
}
|
|
44231
|
+
function getBlockNewPatterns(block, permissions) {
|
|
44232
|
+
const allow = permissions?.allow ?? [];
|
|
44233
|
+
if (Array.isArray(block.generalized_pattern)) {
|
|
44234
|
+
return getNewPatterns(block.generalized_pattern, permissions);
|
|
44235
|
+
}
|
|
44236
|
+
const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
|
|
44237
|
+
const isBash = (block.type.toLowerCase() === "bash" || agentPattern.includes(":BASH(")) && !!block.cmd;
|
|
44238
|
+
if (!isBash) {
|
|
44239
|
+
return getBlockPatterns(block).filter((p) => !isPatternAllowed(permissions, p));
|
|
44240
|
+
}
|
|
44241
|
+
const serverId = getBlockServerId(block);
|
|
44242
|
+
const prefix = serverId === "*" ? "" : `${serverId}:`;
|
|
44243
|
+
const rawSubs = splitShellCommands(block.cmd);
|
|
44244
|
+
const pairs = [];
|
|
44245
|
+
for (const raw of rawSubs) {
|
|
44246
|
+
const parsed = parseBashCmd(raw);
|
|
44247
|
+
if (parsed.length === 0)
|
|
44248
|
+
continue;
|
|
44249
|
+
pairs.push({ raw, pattern: prefix + buildBashPattern(parsed[0]) });
|
|
44250
|
+
}
|
|
44251
|
+
return pairs.filter(({ raw, pattern }) => !isPatternAllowed(permissions, pattern) && !allow.some((rule) => bashRuleMatchesCmd(rule, serverId, raw))).map(({ pattern }) => pattern);
|
|
44252
|
+
}
|
|
44184
44253
|
|
|
44185
44254
|
// src/diff-view.ts
|
|
44186
44255
|
var import_diff_match_patch = __toESM(require_diff_match_patch(), 1);
|
|
@@ -44428,12 +44497,14 @@ function renderDiff(originalContent, modifiedContent, filePath) {
|
|
|
44428
44497
|
var diffStoreByWs = new WeakMap;
|
|
44429
44498
|
function classifyBlock(info) {
|
|
44430
44499
|
const inner = (info.block_type || "").toLowerCase();
|
|
44431
|
-
if (["create", "createfile"].includes(inner))
|
|
44500
|
+
if (["create", "createfile", "write"].includes(inner))
|
|
44432
44501
|
return "create";
|
|
44433
44502
|
if (["modify", "modifyfile", "update", "edit"].includes(inner))
|
|
44434
44503
|
return "edit";
|
|
44435
44504
|
if (["catfile", "read", "readfile"].includes(inner))
|
|
44436
44505
|
return "read";
|
|
44506
|
+
if (["search", "grep"].includes(inner))
|
|
44507
|
+
return "search";
|
|
44437
44508
|
if (inner === "mcp")
|
|
44438
44509
|
return "mcp";
|
|
44439
44510
|
if (["shell", "bash"].includes(inner) || info.cmd)
|
|
@@ -44441,7 +44512,7 @@ function classifyBlock(info) {
|
|
|
44441
44512
|
return "unknown";
|
|
44442
44513
|
}
|
|
44443
44514
|
function blockDisplay(info) {
|
|
44444
|
-
const labels = { create: "File", edit: "Edit", read: "Read File", mcp: "MCP", shell: "Shell" };
|
|
44515
|
+
const labels = { create: "File", edit: "Edit", read: "Read File", search: "Search", mcp: "MCP", shell: "Shell" };
|
|
44445
44516
|
const kind = classifyBlock(info);
|
|
44446
44517
|
const typeLabel = labels[kind] || info.block_type || "Tool";
|
|
44447
44518
|
const skipKeys = new Set([
|
|
@@ -44587,12 +44658,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
|
|
|
44587
44658
|
process.stderr.write(renderDiff(diff.originalContent, diff.modifiedContent, filePath));
|
|
44588
44659
|
}
|
|
44589
44660
|
}
|
|
44590
|
-
const
|
|
44661
|
+
const newPatterns = blocks.flatMap((bi) => getBlockNewPatterns({
|
|
44591
44662
|
type: bi.block_type || "unknown",
|
|
44592
44663
|
generalized_pattern: bi.generalized_pattern,
|
|
44593
44664
|
cmd: bi.cmd
|
|
44594
|
-
}));
|
|
44595
|
-
const newPatterns = getNewPatterns(allPatterns, opts.agentSettings?.permissions);
|
|
44665
|
+
}, opts.agentSettings?.permissions));
|
|
44596
44666
|
const stripPrefix = (p) => p.replace(/^todoai_(edge|cloud):/, "");
|
|
44597
44667
|
const patternHint = newPatterns.length ? ` ${DIM}${newPatterns.map(stripPrefix).join(", ")}${RESET}` : "";
|
|
44598
44668
|
try {
|
|
@@ -44605,11 +44675,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
|
|
|
44605
44675
|
for (const bi of blocks) {
|
|
44606
44676
|
let patterns;
|
|
44607
44677
|
if (response === "r") {
|
|
44608
|
-
patterns =
|
|
44678
|
+
patterns = getBlockNewPatterns({
|
|
44609
44679
|
type: bi.block_type || "unknown",
|
|
44610
44680
|
generalized_pattern: bi.generalized_pattern,
|
|
44611
44681
|
cmd: bi.cmd
|
|
44612
|
-
});
|
|
44682
|
+
}, opts.agentSettings?.permissions);
|
|
44613
44683
|
if (patterns.length > 0) {
|
|
44614
44684
|
process.stderr.write(` ${GREEN}\u2713 Remembering: ${patterns.map(stripPrefix).join(", ")}${RESET}
|
|
44615
44685
|
`);
|
|
@@ -44747,10 +44817,62 @@ ${DIM}[todo:status] ${status}${RESET}
|
|
|
44747
44817
|
}
|
|
44748
44818
|
}
|
|
44749
44819
|
|
|
44820
|
+
// src/list-agents.ts
|
|
44821
|
+
async function listAgentsCommand(api, opts) {
|
|
44822
|
+
const agents = await api.listAgentSettings();
|
|
44823
|
+
if (opts.json) {
|
|
44824
|
+
console.log(JSON.stringify(agents, null, 2));
|
|
44825
|
+
return;
|
|
44826
|
+
}
|
|
44827
|
+
if (!agents.length) {
|
|
44828
|
+
process.stderr.write(`No agents found.
|
|
44829
|
+
`);
|
|
44830
|
+
return;
|
|
44831
|
+
}
|
|
44832
|
+
const rows = agents.map((a) => ({
|
|
44833
|
+
name: getDisplayName(a),
|
|
44834
|
+
id: getItemId(a),
|
|
44835
|
+
model: a.model || "",
|
|
44836
|
+
paths: getAgentWorkspacePaths(a).map(opts.formatPath)
|
|
44837
|
+
}));
|
|
44838
|
+
const nameW = Math.max(4, ...rows.map((r) => r.name.length));
|
|
44839
|
+
const modelW = Math.max(5, ...rows.map((r) => r.model.length));
|
|
44840
|
+
process.stderr.write(`${DIM}${"NAME".padEnd(nameW)} ${"MODEL".padEnd(modelW)} ID${" ".repeat(34)}PATHS${RESET}
|
|
44841
|
+
`);
|
|
44842
|
+
for (const r of rows) {
|
|
44843
|
+
process.stderr.write(`${BRAND}${r.name.padEnd(nameW)}${RESET} ${CYAN}${r.model.padEnd(modelW)}${RESET} ${DIM}${r.id}${RESET} ${r.paths.join(", ")}
|
|
44844
|
+
`);
|
|
44845
|
+
}
|
|
44846
|
+
}
|
|
44847
|
+
|
|
44848
|
+
// src/ensure-edge.ts
|
|
44849
|
+
import { spawn } from "child_process";
|
|
44850
|
+
import fs from "fs";
|
|
44851
|
+
import path2 from "path";
|
|
44852
|
+
import os2 from "os";
|
|
44853
|
+
function ensureEdgeRunning(apiUrl, apiKey) {
|
|
44854
|
+
const logDir = path2.join(os2.homedir(), ".todoforai");
|
|
44855
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
44856
|
+
const logFile = path2.join(logDir, "edge.log");
|
|
44857
|
+
const out = fs.openSync(logFile, "a");
|
|
44858
|
+
const child = spawn("bunx", ["@todoforai/edge", "--api-url", apiUrl, "--api-key", apiKey], {
|
|
44859
|
+
detached: true,
|
|
44860
|
+
stdio: ["ignore", out, out]
|
|
44861
|
+
});
|
|
44862
|
+
child.unref();
|
|
44863
|
+
const pid = child.pid;
|
|
44864
|
+
setTimeout(() => {
|
|
44865
|
+
try {
|
|
44866
|
+
process.kill(pid, 0);
|
|
44867
|
+
console.error(`\x1B[2mStarted edge daemon (pid ${pid}), logs: ${logFile.replace(os2.homedir(), "~")}\x1B[0m`);
|
|
44868
|
+
} catch {}
|
|
44869
|
+
}, 500);
|
|
44870
|
+
}
|
|
44871
|
+
|
|
44750
44872
|
// src/index.ts
|
|
44751
|
-
function formatPathWithTilde(
|
|
44752
|
-
const home =
|
|
44753
|
-
return
|
|
44873
|
+
function formatPathWithTilde(path3) {
|
|
44874
|
+
const home = homedir3();
|
|
44875
|
+
return path3.startsWith(home) ? path3.replace(home, "~") : path3;
|
|
44754
44876
|
}
|
|
44755
44877
|
function getFrontendUrl(apiUrl, projectId, todoId) {
|
|
44756
44878
|
if (apiUrl.includes("localhost:4000") || apiUrl.includes("127.0.0.1:4000")) {
|
|
@@ -44844,8 +44966,8 @@ Cancelled by user (Ctrl+C)
|
|
|
44844
44966
|
return;
|
|
44845
44967
|
}
|
|
44846
44968
|
if (args["reset-config"]) {
|
|
44847
|
-
const { existsSync:
|
|
44848
|
-
if (
|
|
44969
|
+
const { existsSync: existsSync3, unlinkSync } = await import("fs");
|
|
44970
|
+
if (existsSync3(cfg.path)) {
|
|
44849
44971
|
unlinkSync(cfg.path);
|
|
44850
44972
|
console.log(`Configuration reset: ${formatPathWithTilde(cfg.path)}`);
|
|
44851
44973
|
} else
|
|
@@ -44857,21 +44979,6 @@ Cancelled by user (Ctrl+C)
|
|
|
44857
44979
|
console.log(`Default API URL set to: ${args["set-default-api-url"]}`);
|
|
44858
44980
|
return;
|
|
44859
44981
|
}
|
|
44860
|
-
if (args["set-default-api-key"]) {
|
|
44861
|
-
cfg.setDefaultApiKey(args["set-default-api-key"]);
|
|
44862
|
-
console.log("Default API key set");
|
|
44863
|
-
return;
|
|
44864
|
-
}
|
|
44865
|
-
if (args["set-defaults"]) {
|
|
44866
|
-
const url = await readLine(`API URL [${cfg.data.default_api_url || DEFAULT_API_URL}]: `);
|
|
44867
|
-
if (url)
|
|
44868
|
-
cfg.setDefaultApiUrl(url);
|
|
44869
|
-
const key = await readLine("API Key: ");
|
|
44870
|
-
if (key)
|
|
44871
|
-
cfg.setDefaultApiKey(key);
|
|
44872
|
-
console.log("Defaults saved.");
|
|
44873
|
-
return;
|
|
44874
|
-
}
|
|
44875
44982
|
const apiUrl = normalizeApiUrl(args["api-url"] || cfg.data.default_api_url || getEnv("API_URL") || DEFAULT_API_URL);
|
|
44876
44983
|
async function deviceLogin() {
|
|
44877
44984
|
const loginApi = new ApiClient(apiUrl, "");
|
|
@@ -44886,12 +44993,12 @@ Cancelled by user (Ctrl+C)
|
|
|
44886
44993
|
|
|
44887
44994
|
`);
|
|
44888
44995
|
try {
|
|
44889
|
-
const { spawn } = await import("child_process");
|
|
44996
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
44890
44997
|
if (process.platform === "win32") {
|
|
44891
|
-
|
|
44998
|
+
spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
|
|
44892
44999
|
} else {
|
|
44893
45000
|
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
44894
|
-
|
|
45001
|
+
spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
44895
45002
|
}
|
|
44896
45003
|
} catch {}
|
|
44897
45004
|
process.stderr.write(`Waiting for approval (expires in ${Math.round(expiresIn / 60)}min)...
|
|
@@ -44904,7 +45011,7 @@ Cancelled by user (Ctrl+C)
|
|
|
44904
45011
|
const poll = await loginApi.pollDeviceLogin(code);
|
|
44905
45012
|
failures = 0;
|
|
44906
45013
|
if (poll.status === "complete" && poll.apiKey) {
|
|
44907
|
-
|
|
45014
|
+
writeCredential(apiUrl, poll.apiKey);
|
|
44908
45015
|
process.stderr.write(`${GREEN}\u2705 Login successful! API key saved.${RESET}
|
|
44909
45016
|
`);
|
|
44910
45017
|
return poll.apiKey;
|
|
@@ -44927,11 +45034,17 @@ Cancelled by user (Ctrl+C)
|
|
|
44927
45034
|
await deviceLogin();
|
|
44928
45035
|
return;
|
|
44929
45036
|
}
|
|
44930
|
-
let apiKey = args["api-key"] ||
|
|
45037
|
+
let apiKey = args["api-key"] || getEnv("API_KEY") || readCredential(apiUrl) || "";
|
|
44931
45038
|
if (!apiKey) {
|
|
44932
45039
|
apiKey = await deviceLogin();
|
|
44933
45040
|
}
|
|
44934
45041
|
const api = new ApiClient(apiUrl, apiKey);
|
|
45042
|
+
if (!args["no-edge"])
|
|
45043
|
+
ensureEdgeRunning(apiUrl, apiKey);
|
|
45044
|
+
if (args["list-agents"]) {
|
|
45045
|
+
await listAgentsCommand(api, { json: !!args.json, formatPath: formatPathWithTilde });
|
|
45046
|
+
return;
|
|
45047
|
+
}
|
|
44935
45048
|
if (args.inspect) {
|
|
44936
45049
|
const todoId = args.inspect;
|
|
44937
45050
|
const todo2 = await api.getTodo(todoId);
|