@todoforai/cli 0.1.2 → 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 +19 -3
- package/dist/todoai.js +195 -72
- 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
|
|
@@ -66,14 +82,14 @@ todoai --resume <todo-id> # resume specific todo
|
|
|
66
82
|
--continue, -c Continue most recent todo
|
|
67
83
|
--non-interactive, -n Run to completion and exit
|
|
68
84
|
--dangerously-skip-permissions Auto-approve all blocks (CI/benchmarks)
|
|
85
|
+
--allow-all Set permissions to allow all tools (no approval needed)
|
|
69
86
|
--no-watch Create todo and exit
|
|
87
|
+
--no-edge Do not auto-spawn edge daemon
|
|
70
88
|
--json Output as JSON
|
|
71
89
|
--safe Validate API key upfront
|
|
72
90
|
--debug, -d Debug output
|
|
73
91
|
--show-config Show config
|
|
74
|
-
--set-defaults Interactive defaults setup
|
|
75
92
|
--set-default-api-url Set default API URL
|
|
76
|
-
--set-default-api-key Set default API key
|
|
77
93
|
--reset-config Reset config file
|
|
78
94
|
--help, -h Show this help
|
|
79
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)
|
|
@@ -43059,14 +43062,14 @@ Options:
|
|
|
43059
43062
|
--continue, -c Continue most recent todo
|
|
43060
43063
|
--non-interactive, -n Run to completion and exit without interactive prompt
|
|
43061
43064
|
--dangerously-skip-permissions Auto-approve all blocks (for CI/benchmarks)
|
|
43065
|
+
--allow-all Set permissions to allow all tools (no approval needed)
|
|
43062
43066
|
--no-watch Create todo and exit
|
|
43067
|
+
--no-edge Do not auto-spawn edge daemon
|
|
43063
43068
|
--json Output as JSON
|
|
43064
43069
|
--safe Validate API key upfront
|
|
43065
43070
|
--debug, -d Debug output
|
|
43066
43071
|
--show-config Show config
|
|
43067
|
-
--set-defaults Interactive defaults setup
|
|
43068
43072
|
--set-default-api-url Set default API URL
|
|
43069
|
-
--set-default-api-key Set default API key
|
|
43070
43073
|
--reset-config Reset config file
|
|
43071
43074
|
--help, -h Show this help
|
|
43072
43075
|
`);
|
|
@@ -43078,6 +43081,7 @@ function parseCliArgs() {
|
|
|
43078
43081
|
path: { type: "string", default: "." },
|
|
43079
43082
|
project: { type: "string" },
|
|
43080
43083
|
agent: { type: "string", short: "a" },
|
|
43084
|
+
"list-agents": { type: "boolean", default: false },
|
|
43081
43085
|
"api-url": { type: "string" },
|
|
43082
43086
|
"api-key": { type: "string" },
|
|
43083
43087
|
inspect: { type: "string", short: "i" },
|
|
@@ -43087,14 +43091,14 @@ function parseCliArgs() {
|
|
|
43087
43091
|
continue: { type: "boolean", short: "c", default: false },
|
|
43088
43092
|
"non-interactive": { type: "boolean", short: "n", default: false },
|
|
43089
43093
|
"dangerously-skip-permissions": { type: "boolean", default: false },
|
|
43094
|
+
"allow-all": { type: "boolean", default: false },
|
|
43090
43095
|
"no-watch": { type: "boolean", default: false },
|
|
43096
|
+
"no-edge": { type: "boolean", default: false },
|
|
43091
43097
|
json: { type: "boolean", default: false },
|
|
43092
43098
|
safe: { type: "boolean", default: false },
|
|
43093
43099
|
debug: { type: "boolean", short: "d", default: false },
|
|
43094
43100
|
"show-config": { type: "boolean", default: false },
|
|
43095
|
-
"set-defaults": { type: "boolean", default: false },
|
|
43096
43101
|
"set-default-api-url": { type: "string" },
|
|
43097
|
-
"set-default-api-key": { type: "string" },
|
|
43098
43102
|
"reset-config": { type: "boolean", default: false },
|
|
43099
43103
|
"config-path": { type: "string" },
|
|
43100
43104
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -43491,18 +43495,6 @@ function getConfigDir() {
|
|
|
43491
43495
|
const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
43492
43496
|
return join(xdg, "todoai-cli");
|
|
43493
43497
|
}
|
|
43494
|
-
function obfuscate(s) {
|
|
43495
|
-
return s ? Buffer.from(s, "utf-8").toString("base64") : s;
|
|
43496
|
-
}
|
|
43497
|
-
function deobfuscate(s) {
|
|
43498
|
-
if (!s)
|
|
43499
|
-
return s;
|
|
43500
|
-
try {
|
|
43501
|
-
return Buffer.from(s, "base64").toString("utf-8");
|
|
43502
|
-
} catch {
|
|
43503
|
-
return s;
|
|
43504
|
-
}
|
|
43505
|
-
}
|
|
43506
43498
|
function defaultConfig() {
|
|
43507
43499
|
return {
|
|
43508
43500
|
default_project_id: null,
|
|
@@ -43511,7 +43503,6 @@ function defaultConfig() {
|
|
|
43511
43503
|
default_agent_settings: null,
|
|
43512
43504
|
default_agent_settings_updated_at: null,
|
|
43513
43505
|
default_api_url: null,
|
|
43514
|
-
default_api_key: null,
|
|
43515
43506
|
recent_projects: [],
|
|
43516
43507
|
recent_agents: [],
|
|
43517
43508
|
last_todo_id: null,
|
|
@@ -43536,8 +43527,7 @@ class ConfigStore {
|
|
|
43536
43527
|
return defaultConfig();
|
|
43537
43528
|
try {
|
|
43538
43529
|
const raw = JSON.parse(readFileSync(this.path, "utf-8"));
|
|
43539
|
-
|
|
43540
|
-
raw.default_api_key = deobfuscate(raw.default_api_key);
|
|
43530
|
+
delete raw.default_api_key;
|
|
43541
43531
|
return { ...defaultConfig(), ...raw };
|
|
43542
43532
|
} catch {
|
|
43543
43533
|
return defaultConfig();
|
|
@@ -43546,10 +43536,7 @@ class ConfigStore {
|
|
|
43546
43536
|
save() {
|
|
43547
43537
|
try {
|
|
43548
43538
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
43549
|
-
|
|
43550
|
-
if (out.default_api_key)
|
|
43551
|
-
out.default_api_key = obfuscate(out.default_api_key);
|
|
43552
|
-
writeFileSync(this.path, JSON.stringify(out, null, 2), "utf-8");
|
|
43539
|
+
writeFileSync(this.path, JSON.stringify(this.data, null, 2), "utf-8");
|
|
43553
43540
|
} catch {}
|
|
43554
43541
|
}
|
|
43555
43542
|
setDefaultProject(id, name) {
|
|
@@ -43573,10 +43560,6 @@ class ConfigStore {
|
|
|
43573
43560
|
this.data.default_api_url = url;
|
|
43574
43561
|
this.save();
|
|
43575
43562
|
}
|
|
43576
|
-
setDefaultApiKey(key) {
|
|
43577
|
-
this.data.default_api_key = key;
|
|
43578
|
-
this.save();
|
|
43579
|
-
}
|
|
43580
43563
|
addToHistory(input) {
|
|
43581
43564
|
const trimmed = input.trim();
|
|
43582
43565
|
if (!trimmed)
|
|
@@ -43593,6 +43576,30 @@ class ConfigStore {
|
|
|
43593
43576
|
}
|
|
43594
43577
|
}
|
|
43595
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
|
+
|
|
43596
43603
|
// src/logo.ts
|
|
43597
43604
|
var LETTERS = {
|
|
43598
43605
|
t: [" x ", "xxxx", " x ", " xll", " xll", " xxx"],
|
|
@@ -43911,6 +43918,12 @@ function splitShellCommands(input) {
|
|
|
43911
43918
|
const len = input.length;
|
|
43912
43919
|
while (i < len) {
|
|
43913
43920
|
const ch = input[i];
|
|
43921
|
+
if (ch === "\\" && i + 1 < len && input[i + 1] === `
|
|
43922
|
+
`) {
|
|
43923
|
+
current += " ";
|
|
43924
|
+
i += 2;
|
|
43925
|
+
continue;
|
|
43926
|
+
}
|
|
43914
43927
|
if (ch === "\\" && i + 1 < len) {
|
|
43915
43928
|
current += ch + input[i + 1];
|
|
43916
43929
|
i += 2;
|
|
@@ -44023,6 +44036,11 @@ function splitTokens(cmd) {
|
|
|
44023
44036
|
const len = cmd.length;
|
|
44024
44037
|
while (i < len) {
|
|
44025
44038
|
const ch = cmd[i];
|
|
44039
|
+
if (ch === "\\" && i + 1 < len && cmd[i + 1] === `
|
|
44040
|
+
`) {
|
|
44041
|
+
i += 2;
|
|
44042
|
+
continue;
|
|
44043
|
+
}
|
|
44026
44044
|
if (ch === "\\" && i + 1 < len) {
|
|
44027
44045
|
current += ch + cmd[i + 1];
|
|
44028
44046
|
i += 2;
|
|
@@ -44113,29 +44131,40 @@ function parseBashCmd(fullCmd) {
|
|
|
44113
44131
|
function generalizeBashCmd(fullCmd) {
|
|
44114
44132
|
return parseBashCmd(fullCmd).map((p) => buildBashPattern(p));
|
|
44115
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"]);
|
|
44116
44140
|
function getBlockPatterns(block) {
|
|
44117
44141
|
if (Array.isArray(block.generalized_pattern))
|
|
44118
44142
|
return block.generalized_pattern;
|
|
44119
|
-
|
|
44120
|
-
|
|
44121
|
-
|
|
44122
|
-
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);
|
|
44123
44147
|
return generalizeBashCmd(block.cmd).map((p) => prefix + p);
|
|
44124
44148
|
}
|
|
44125
44149
|
if (block.generalized_pattern)
|
|
44126
44150
|
return [block.generalized_pattern];
|
|
44127
44151
|
return [`${block.type}(*)`];
|
|
44128
44152
|
}
|
|
44153
|
+
function getBlockServerId(block) {
|
|
44154
|
+
const prefix = getBlockServerPrefix(block);
|
|
44155
|
+
return prefix ? prefix.slice(0, -1) : "*";
|
|
44156
|
+
}
|
|
44129
44157
|
|
|
44130
44158
|
// ../packages/shared-fbe/src/permissionUtils.ts
|
|
44131
44159
|
function parsePattern(pattern) {
|
|
44132
44160
|
const colonIndex = pattern.indexOf(":");
|
|
44133
44161
|
if (colonIndex === -1)
|
|
44134
44162
|
return null;
|
|
44135
|
-
|
|
44136
|
-
|
|
44137
|
-
toolName: pattern
|
|
44138
|
-
}
|
|
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) };
|
|
44139
44168
|
}
|
|
44140
44169
|
function serverIdMatches(ruleServerId, targetServerId) {
|
|
44141
44170
|
if (ruleServerId === targetServerId)
|
|
@@ -44168,6 +44197,26 @@ function patternMatches(rulePattern, targetPattern) {
|
|
|
44168
44197
|
}
|
|
44169
44198
|
return false;
|
|
44170
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
|
+
}
|
|
44171
44220
|
function isPatternInList(list, pattern) {
|
|
44172
44221
|
if (!list)
|
|
44173
44222
|
return false;
|
|
@@ -44179,6 +44228,28 @@ function isPatternAllowed(permissions, pattern) {
|
|
|
44179
44228
|
function getNewPatterns(patterns, permissions) {
|
|
44180
44229
|
return patterns.filter((p) => !isPatternAllowed(permissions, p));
|
|
44181
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
|
+
}
|
|
44182
44253
|
|
|
44183
44254
|
// src/diff-view.ts
|
|
44184
44255
|
var import_diff_match_patch = __toESM(require_diff_match_patch(), 1);
|
|
@@ -44426,12 +44497,14 @@ function renderDiff(originalContent, modifiedContent, filePath) {
|
|
|
44426
44497
|
var diffStoreByWs = new WeakMap;
|
|
44427
44498
|
function classifyBlock(info) {
|
|
44428
44499
|
const inner = (info.block_type || "").toLowerCase();
|
|
44429
|
-
if (["create", "createfile"].includes(inner))
|
|
44500
|
+
if (["create", "createfile", "write"].includes(inner))
|
|
44430
44501
|
return "create";
|
|
44431
44502
|
if (["modify", "modifyfile", "update", "edit"].includes(inner))
|
|
44432
44503
|
return "edit";
|
|
44433
44504
|
if (["catfile", "read", "readfile"].includes(inner))
|
|
44434
44505
|
return "read";
|
|
44506
|
+
if (["search", "grep"].includes(inner))
|
|
44507
|
+
return "search";
|
|
44435
44508
|
if (inner === "mcp")
|
|
44436
44509
|
return "mcp";
|
|
44437
44510
|
if (["shell", "bash"].includes(inner) || info.cmd)
|
|
@@ -44439,7 +44512,7 @@ function classifyBlock(info) {
|
|
|
44439
44512
|
return "unknown";
|
|
44440
44513
|
}
|
|
44441
44514
|
function blockDisplay(info) {
|
|
44442
|
-
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" };
|
|
44443
44516
|
const kind = classifyBlock(info);
|
|
44444
44517
|
const typeLabel = labels[kind] || info.block_type || "Tool";
|
|
44445
44518
|
const skipKeys = new Set([
|
|
@@ -44585,12 +44658,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
|
|
|
44585
44658
|
process.stderr.write(renderDiff(diff.originalContent, diff.modifiedContent, filePath));
|
|
44586
44659
|
}
|
|
44587
44660
|
}
|
|
44588
|
-
const
|
|
44661
|
+
const newPatterns = blocks.flatMap((bi) => getBlockNewPatterns({
|
|
44589
44662
|
type: bi.block_type || "unknown",
|
|
44590
44663
|
generalized_pattern: bi.generalized_pattern,
|
|
44591
44664
|
cmd: bi.cmd
|
|
44592
|
-
}));
|
|
44593
|
-
const newPatterns = getNewPatterns(allPatterns, opts.agentSettings?.permissions);
|
|
44665
|
+
}, opts.agentSettings?.permissions));
|
|
44594
44666
|
const stripPrefix = (p) => p.replace(/^todoai_(edge|cloud):/, "");
|
|
44595
44667
|
const patternHint = newPatterns.length ? ` ${DIM}${newPatterns.map(stripPrefix).join(", ")}${RESET}` : "";
|
|
44596
44668
|
try {
|
|
@@ -44603,11 +44675,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
|
|
|
44603
44675
|
for (const bi of blocks) {
|
|
44604
44676
|
let patterns;
|
|
44605
44677
|
if (response === "r") {
|
|
44606
|
-
patterns =
|
|
44678
|
+
patterns = getBlockNewPatterns({
|
|
44607
44679
|
type: bi.block_type || "unknown",
|
|
44608
44680
|
generalized_pattern: bi.generalized_pattern,
|
|
44609
44681
|
cmd: bi.cmd
|
|
44610
|
-
});
|
|
44682
|
+
}, opts.agentSettings?.permissions);
|
|
44611
44683
|
if (patterns.length > 0) {
|
|
44612
44684
|
process.stderr.write(` ${GREEN}\u2713 Remembering: ${patterns.map(stripPrefix).join(", ")}${RESET}
|
|
44613
44685
|
`);
|
|
@@ -44745,10 +44817,62 @@ ${DIM}[todo:status] ${status}${RESET}
|
|
|
44745
44817
|
}
|
|
44746
44818
|
}
|
|
44747
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
|
+
|
|
44748
44872
|
// src/index.ts
|
|
44749
|
-
function formatPathWithTilde(
|
|
44750
|
-
const home =
|
|
44751
|
-
return
|
|
44873
|
+
function formatPathWithTilde(path3) {
|
|
44874
|
+
const home = homedir3();
|
|
44875
|
+
return path3.startsWith(home) ? path3.replace(home, "~") : path3;
|
|
44752
44876
|
}
|
|
44753
44877
|
function getFrontendUrl(apiUrl, projectId, todoId) {
|
|
44754
44878
|
if (apiUrl.includes("localhost:4000") || apiUrl.includes("127.0.0.1:4000")) {
|
|
@@ -44842,8 +44966,8 @@ Cancelled by user (Ctrl+C)
|
|
|
44842
44966
|
return;
|
|
44843
44967
|
}
|
|
44844
44968
|
if (args["reset-config"]) {
|
|
44845
|
-
const { existsSync:
|
|
44846
|
-
if (
|
|
44969
|
+
const { existsSync: existsSync3, unlinkSync } = await import("fs");
|
|
44970
|
+
if (existsSync3(cfg.path)) {
|
|
44847
44971
|
unlinkSync(cfg.path);
|
|
44848
44972
|
console.log(`Configuration reset: ${formatPathWithTilde(cfg.path)}`);
|
|
44849
44973
|
} else
|
|
@@ -44855,21 +44979,6 @@ Cancelled by user (Ctrl+C)
|
|
|
44855
44979
|
console.log(`Default API URL set to: ${args["set-default-api-url"]}`);
|
|
44856
44980
|
return;
|
|
44857
44981
|
}
|
|
44858
|
-
if (args["set-default-api-key"]) {
|
|
44859
|
-
cfg.setDefaultApiKey(args["set-default-api-key"]);
|
|
44860
|
-
console.log("Default API key set");
|
|
44861
|
-
return;
|
|
44862
|
-
}
|
|
44863
|
-
if (args["set-defaults"]) {
|
|
44864
|
-
const url = await readLine(`API URL [${cfg.data.default_api_url || DEFAULT_API_URL}]: `);
|
|
44865
|
-
if (url)
|
|
44866
|
-
cfg.setDefaultApiUrl(url);
|
|
44867
|
-
const key = await readLine("API Key: ");
|
|
44868
|
-
if (key)
|
|
44869
|
-
cfg.setDefaultApiKey(key);
|
|
44870
|
-
console.log("Defaults saved.");
|
|
44871
|
-
return;
|
|
44872
|
-
}
|
|
44873
44982
|
const apiUrl = normalizeApiUrl(args["api-url"] || cfg.data.default_api_url || getEnv("API_URL") || DEFAULT_API_URL);
|
|
44874
44983
|
async function deviceLogin() {
|
|
44875
44984
|
const loginApi = new ApiClient(apiUrl, "");
|
|
@@ -44884,12 +44993,12 @@ Cancelled by user (Ctrl+C)
|
|
|
44884
44993
|
|
|
44885
44994
|
`);
|
|
44886
44995
|
try {
|
|
44887
|
-
const { spawn } = await import("child_process");
|
|
44996
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
44888
44997
|
if (process.platform === "win32") {
|
|
44889
|
-
|
|
44998
|
+
spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
|
|
44890
44999
|
} else {
|
|
44891
45000
|
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
44892
|
-
|
|
45001
|
+
spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
44893
45002
|
}
|
|
44894
45003
|
} catch {}
|
|
44895
45004
|
process.stderr.write(`Waiting for approval (expires in ${Math.round(expiresIn / 60)}min)...
|
|
@@ -44902,7 +45011,7 @@ Cancelled by user (Ctrl+C)
|
|
|
44902
45011
|
const poll = await loginApi.pollDeviceLogin(code);
|
|
44903
45012
|
failures = 0;
|
|
44904
45013
|
if (poll.status === "complete" && poll.apiKey) {
|
|
44905
|
-
|
|
45014
|
+
writeCredential(apiUrl, poll.apiKey);
|
|
44906
45015
|
process.stderr.write(`${GREEN}\u2705 Login successful! API key saved.${RESET}
|
|
44907
45016
|
`);
|
|
44908
45017
|
return poll.apiKey;
|
|
@@ -44925,11 +45034,17 @@ Cancelled by user (Ctrl+C)
|
|
|
44925
45034
|
await deviceLogin();
|
|
44926
45035
|
return;
|
|
44927
45036
|
}
|
|
44928
|
-
let apiKey = args["api-key"] ||
|
|
45037
|
+
let apiKey = args["api-key"] || getEnv("API_KEY") || readCredential(apiUrl) || "";
|
|
44929
45038
|
if (!apiKey) {
|
|
44930
45039
|
apiKey = await deviceLogin();
|
|
44931
45040
|
}
|
|
44932
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
|
+
}
|
|
44933
45048
|
if (args.inspect) {
|
|
44934
45049
|
const todoId = args.inspect;
|
|
44935
45050
|
const todo2 = await api.getTodo(todoId);
|
|
@@ -44995,7 +45110,11 @@ Cancelled by user (Ctrl+C)
|
|
|
44995
45110
|
const ws2 = new FrontendWebSocket(apiUrl, apiKey);
|
|
44996
45111
|
await ws2.connect();
|
|
44997
45112
|
const autoApprove = !!args["dangerously-skip-permissions"];
|
|
44998
|
-
|
|
45113
|
+
let agent2 = todo2.agentSettings || { id: todo2.agentSettingsId };
|
|
45114
|
+
if (args["allow-all"]) {
|
|
45115
|
+
const perms = agent2.permissions || { allow: [], ask: [], deny: [] };
|
|
45116
|
+
agent2 = { ...agent2, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
|
|
45117
|
+
}
|
|
44999
45118
|
await watchTodo(ws2, todoId, projectId2, {
|
|
45000
45119
|
json: !!args.json,
|
|
45001
45120
|
autoApprove,
|
|
@@ -45005,7 +45124,7 @@ Cancelled by user (Ctrl+C)
|
|
|
45005
45124
|
process.stderr.write(`
|
|
45006
45125
|
${"\u2500".repeat(40)}
|
|
45007
45126
|
`);
|
|
45008
|
-
await interactiveLoop(ws2, api, todoId, projectId2, agent2, !!args.json, autoApprove);
|
|
45127
|
+
await interactiveLoop(ws2, api, todoId, projectId2, agent2, !!args.json, autoApprove, cfg);
|
|
45009
45128
|
}
|
|
45010
45129
|
await ws2.close();
|
|
45011
45130
|
}
|
|
@@ -45135,6 +45254,10 @@ Resumed todo: ${todoId}
|
|
|
45135
45254
|
await ws.connect();
|
|
45136
45255
|
if (args.model)
|
|
45137
45256
|
agent = { ...agent, model: args.model };
|
|
45257
|
+
if (args["allow-all"]) {
|
|
45258
|
+
const perms = agent.permissions || { allow: [], ask: [], deny: [] };
|
|
45259
|
+
agent = { ...agent, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
|
|
45260
|
+
}
|
|
45138
45261
|
cfg.addToHistory(content);
|
|
45139
45262
|
const todo = await api.addMessage(projectId, content, agent);
|
|
45140
45263
|
const actualTodoId = todo.id || crypto.randomUUID();
|