@todoforai/cli 0.1.3 → 0.1.5

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 +18 -3
  2. package/dist/todoai.js +237 -76
  3. 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 join2(regexps, separator = "|") {
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(join2(terminators), true);
1903
+ this.matcherRe = langRe(join3(terminators), true);
1904
1904
  this.lastIndex = 0;
1905
1905
  }
1906
1906
  exec(s) {
@@ -12960,7 +12960,7 @@ var require_http = __commonJS((exports, module) => {
12960
12960
  return joined;
12961
12961
  }
12962
12962
  function http(hljs) {
12963
- const VERSION = "HTTP/(2|1\\.[01])";
12963
+ const VERSION2 = "HTTP/(2|1\\.[01])";
12964
12964
  const HEADER_NAME = /[A-Za-z][A-Za-z0-9-]*/;
12965
12965
  const HEADER = {
12966
12966
  className: "attribute",
@@ -12992,12 +12992,12 @@ var require_http = __commonJS((exports, module) => {
12992
12992
  illegal: /\S/,
12993
12993
  contains: [
12994
12994
  {
12995
- begin: "^(?=" + VERSION + " \\d{3})",
12995
+ begin: "^(?=" + VERSION2 + " \\d{3})",
12996
12996
  end: /$/,
12997
12997
  contains: [
12998
12998
  {
12999
12999
  className: "meta",
13000
- begin: VERSION
13000
+ begin: VERSION2
13001
13001
  },
13002
13002
  {
13003
13003
  className: "number",
@@ -13011,7 +13011,7 @@ var require_http = __commonJS((exports, module) => {
13011
13011
  }
13012
13012
  },
13013
13013
  {
13014
- begin: "(?=^[A-Z]+ (.*?) " + VERSION + "$)",
13014
+ begin: "(?=^[A-Z]+ (.*?) " + VERSION2 + "$)",
13015
13015
  end: /$/,
13016
13016
  contains: [
13017
13017
  {
@@ -13023,7 +13023,7 @@ var require_http = __commonJS((exports, module) => {
13023
13023
  },
13024
13024
  {
13025
13025
  className: "meta",
13026
- begin: VERSION
13026
+ begin: VERSION2
13027
13027
  },
13028
13028
  {
13029
13029
  className: "keyword",
@@ -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 homedir2 } from "os";
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
@@ -43027,7 +43028,45 @@ class FrontendWebSocket {
43027
43028
 
43028
43029
  // src/args.ts
43029
43030
  import { parseArgs } from "util";
43031
+ // package.json
43032
+ var package_default = {
43033
+ name: "@todoforai/cli",
43034
+ version: "0.1.3",
43035
+ type: "module",
43036
+ bin: {
43037
+ todoai: "dist/todoai.js"
43038
+ },
43039
+ files: ["dist/todoai.js"],
43040
+ scripts: {
43041
+ build: "bun build src/index.ts --target=bun --outfile dist/todoai.js --external ws",
43042
+ prepublishOnly: "bun run build",
43043
+ start: "bun run src/index.ts",
43044
+ dev: "bun run src/index.ts",
43045
+ postinstall: "rm -rf node_modules/@todoforai/edge && ln -s ../../../edge/bun node_modules/@todoforai/edge"
43046
+ },
43047
+ dependencies: {
43048
+ "cli-highlight": "^2.1.11",
43049
+ "diff-match-patch": "^1.0.5",
43050
+ ws: "^8.18.0"
43051
+ },
43052
+ peerDependencies: {
43053
+ "@todoforai/edge": "file:../edge/bun"
43054
+ },
43055
+ peerDependenciesMeta: {
43056
+ "@todoforai/edge": {
43057
+ optional: true
43058
+ }
43059
+ },
43060
+ devDependencies: {
43061
+ "@types/ws": "^8.5.13",
43062
+ "@todoforai/edge": "file:../edge/bun",
43063
+ typescript: "^5.7.0"
43064
+ }
43065
+ };
43066
+
43067
+ // src/args.ts
43030
43068
  var DEFAULT_API_URL = "https://api.todofor.ai";
43069
+ var VERSION = package_default.version;
43031
43070
  function getEnv(name) {
43032
43071
  return process.env[`TODOFORAI_${name}`] || process.env[`TODO4AI_${name}`] || "";
43033
43072
  }
@@ -43045,11 +43084,13 @@ Usage:
43045
43084
  todoai --resume <todo-id> # Resume specific todo
43046
43085
  todoai --inspect <todo-id> # Print full chat log (read-only)
43047
43086
  todoai --template <id> [--input k=v] # Start from a registry template
43087
+ todoai --list-agents # List available agents and exit
43048
43088
 
43049
43089
  Options:
43050
43090
  --path <dir> Workspace path (default: cwd)
43051
43091
  --project <id> Project ID
43052
43092
  --agent, -a <name> Agent name (partial match)
43093
+ --list-agents List available agents (name, id, workspace paths) and exit
43053
43094
  --api-url <url> API URL
43054
43095
  --api-key <key> API key
43055
43096
  --inspect, -i <todo-id> Print full chat log (read-only, no interactive)
@@ -43061,14 +43102,14 @@ Options:
43061
43102
  --dangerously-skip-permissions Auto-approve all blocks (for CI/benchmarks)
43062
43103
  --allow-all Set permissions to allow all tools (no approval needed)
43063
43104
  --no-watch Create todo and exit
43105
+ --no-edge Do not auto-spawn edge daemon
43064
43106
  --json Output as JSON
43065
43107
  --safe Validate API key upfront
43066
43108
  --debug, -d Debug output
43067
43109
  --show-config Show config
43068
- --set-defaults Interactive defaults setup
43069
43110
  --set-default-api-url Set default API URL
43070
- --set-default-api-key Set default API key
43071
43111
  --reset-config Reset config file
43112
+ --version, -v Print version and exit
43072
43113
  --help, -h Show this help
43073
43114
  `);
43074
43115
  }
@@ -43079,6 +43120,7 @@ function parseCliArgs() {
43079
43120
  path: { type: "string", default: "." },
43080
43121
  project: { type: "string" },
43081
43122
  agent: { type: "string", short: "a" },
43123
+ "list-agents": { type: "boolean", default: false },
43082
43124
  "api-url": { type: "string" },
43083
43125
  "api-key": { type: "string" },
43084
43126
  inspect: { type: "string", short: "i" },
@@ -43090,16 +43132,16 @@ function parseCliArgs() {
43090
43132
  "dangerously-skip-permissions": { type: "boolean", default: false },
43091
43133
  "allow-all": { type: "boolean", default: false },
43092
43134
  "no-watch": { type: "boolean", default: false },
43135
+ "no-edge": { type: "boolean", default: false },
43093
43136
  json: { type: "boolean", default: false },
43094
43137
  safe: { type: "boolean", default: false },
43095
43138
  debug: { type: "boolean", short: "d", default: false },
43096
43139
  "show-config": { type: "boolean", default: false },
43097
- "set-defaults": { type: "boolean", default: false },
43098
43140
  "set-default-api-url": { type: "string" },
43099
- "set-default-api-key": { type: "string" },
43100
43141
  "reset-config": { type: "boolean", default: false },
43101
43142
  "config-path": { type: "string" },
43102
- help: { type: "boolean", short: "h", default: false }
43143
+ help: { type: "boolean", short: "h", default: false },
43144
+ version: { type: "boolean", short: "v", default: false }
43103
43145
  },
43104
43146
  allowPositionals: true,
43105
43147
  strict: false
@@ -43493,18 +43535,6 @@ function getConfigDir() {
43493
43535
  const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
43494
43536
  return join(xdg, "todoai-cli");
43495
43537
  }
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
43538
  function defaultConfig() {
43509
43539
  return {
43510
43540
  default_project_id: null,
@@ -43513,7 +43543,6 @@ function defaultConfig() {
43513
43543
  default_agent_settings: null,
43514
43544
  default_agent_settings_updated_at: null,
43515
43545
  default_api_url: null,
43516
- default_api_key: null,
43517
43546
  recent_projects: [],
43518
43547
  recent_agents: [],
43519
43548
  last_todo_id: null,
@@ -43538,8 +43567,7 @@ class ConfigStore {
43538
43567
  return defaultConfig();
43539
43568
  try {
43540
43569
  const raw = JSON.parse(readFileSync(this.path, "utf-8"));
43541
- if (raw.default_api_key)
43542
- raw.default_api_key = deobfuscate(raw.default_api_key);
43570
+ delete raw.default_api_key;
43543
43571
  return { ...defaultConfig(), ...raw };
43544
43572
  } catch {
43545
43573
  return defaultConfig();
@@ -43548,10 +43576,7 @@ class ConfigStore {
43548
43576
  save() {
43549
43577
  try {
43550
43578
  mkdirSync(dirname(this.path), { recursive: true });
43551
- const out = { ...this.data };
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");
43579
+ writeFileSync(this.path, JSON.stringify(this.data, null, 2), "utf-8");
43555
43580
  } catch {}
43556
43581
  }
43557
43582
  setDefaultProject(id, name) {
@@ -43575,10 +43600,6 @@ class ConfigStore {
43575
43600
  this.data.default_api_url = url;
43576
43601
  this.save();
43577
43602
  }
43578
- setDefaultApiKey(key) {
43579
- this.data.default_api_key = key;
43580
- this.save();
43581
- }
43582
43603
  addToHistory(input) {
43583
43604
  const trimmed = input.trim();
43584
43605
  if (!trimmed)
@@ -43595,6 +43616,30 @@ class ConfigStore {
43595
43616
  }
43596
43617
  }
43597
43618
 
43619
+ // src/credentials.ts
43620
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
43621
+ import { dirname as dirname2, join as join2 } from "path";
43622
+ import { homedir as homedir2 } from "os";
43623
+ var CREDENTIALS_PATH2 = join2(homedir2(), ".todoforai", "credentials.json");
43624
+ function read() {
43625
+ if (!existsSync2(CREDENTIALS_PATH2))
43626
+ return {};
43627
+ try {
43628
+ return JSON.parse(readFileSync2(CREDENTIALS_PATH2, "utf-8"));
43629
+ } catch {
43630
+ return {};
43631
+ }
43632
+ }
43633
+ function readCredential(apiUrl) {
43634
+ return read()[apiUrl] || "";
43635
+ }
43636
+ function writeCredential(apiUrl, apiKey) {
43637
+ const data = read();
43638
+ data[apiUrl] = apiKey;
43639
+ mkdirSync2(dirname2(CREDENTIALS_PATH2), { recursive: true });
43640
+ writeFileSync2(CREDENTIALS_PATH2, JSON.stringify(data, null, 2), "utf-8");
43641
+ }
43642
+
43598
43643
  // src/logo.ts
43599
43644
  var LETTERS = {
43600
43645
  t: [" x ", "xxxx", " x ", " xll", " xll", " xxx"],
@@ -43913,6 +43958,12 @@ function splitShellCommands(input) {
43913
43958
  const len = input.length;
43914
43959
  while (i < len) {
43915
43960
  const ch = input[i];
43961
+ if (ch === "\\" && i + 1 < len && input[i + 1] === `
43962
+ `) {
43963
+ current += " ";
43964
+ i += 2;
43965
+ continue;
43966
+ }
43916
43967
  if (ch === "\\" && i + 1 < len) {
43917
43968
  current += ch + input[i + 1];
43918
43969
  i += 2;
@@ -44025,6 +44076,11 @@ function splitTokens(cmd) {
44025
44076
  const len = cmd.length;
44026
44077
  while (i < len) {
44027
44078
  const ch = cmd[i];
44079
+ if (ch === "\\" && i + 1 < len && cmd[i + 1] === `
44080
+ `) {
44081
+ i += 2;
44082
+ continue;
44083
+ }
44028
44084
  if (ch === "\\" && i + 1 < len) {
44029
44085
  current += ch + cmd[i + 1];
44030
44086
  i += 2;
@@ -44115,29 +44171,40 @@ function parseBashCmd(fullCmd) {
44115
44171
  function generalizeBashCmd(fullCmd) {
44116
44172
  return parseBashCmd(fullCmd).map((p) => buildBashPattern(p));
44117
44173
  }
44174
+ function getBlockServerPrefix(block) {
44175
+ const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
44176
+ const prefixMatch = agentPattern.match(/^(.*?:)BASH(?:\(|$)/);
44177
+ return prefixMatch ? prefixMatch[1] : "";
44178
+ }
44179
+ var BASH_LIKE_BLOCK_TYPES = new Set(["bash", "machine_exec"]);
44118
44180
  function getBlockPatterns(block) {
44119
44181
  if (Array.isArray(block.generalized_pattern))
44120
44182
  return block.generalized_pattern;
44121
- if (block.type.toLowerCase() === "bash" && block.cmd) {
44122
- const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
44123
- const prefixMatch = agentPattern.match(/^(.*?:)BASH\(/);
44124
- const prefix = prefixMatch ? prefixMatch[1] : "";
44183
+ const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
44184
+ const isBashLike = BASH_LIKE_BLOCK_TYPES.has(block.type.toLowerCase()) || /:BASH(\(|$)/.test(agentPattern);
44185
+ if (isBashLike && block.cmd) {
44186
+ const prefix = getBlockServerPrefix(block);
44125
44187
  return generalizeBashCmd(block.cmd).map((p) => prefix + p);
44126
44188
  }
44127
44189
  if (block.generalized_pattern)
44128
44190
  return [block.generalized_pattern];
44129
44191
  return [`${block.type}(*)`];
44130
44192
  }
44193
+ function getBlockServerId(block) {
44194
+ const prefix = getBlockServerPrefix(block);
44195
+ return prefix ? prefix.slice(0, -1) : "*";
44196
+ }
44131
44197
 
44132
44198
  // ../packages/shared-fbe/src/permissionUtils.ts
44133
44199
  function parsePattern(pattern) {
44134
44200
  const colonIndex = pattern.indexOf(":");
44135
44201
  if (colonIndex === -1)
44136
44202
  return null;
44137
- return {
44138
- serverId: pattern.slice(0, colonIndex),
44139
- toolName: pattern.slice(colonIndex + 1)
44140
- };
44203
+ const serverId = pattern.slice(0, colonIndex);
44204
+ if (!/^[A-Za-z0-9_*-]+$/.test(serverId)) {
44205
+ return { serverId: "*", toolName: pattern };
44206
+ }
44207
+ return { serverId, toolName: pattern.slice(colonIndex + 1) };
44141
44208
  }
44142
44209
  function serverIdMatches(ruleServerId, targetServerId) {
44143
44210
  if (ruleServerId === targetServerId)
@@ -44170,6 +44237,26 @@ function patternMatches(rulePattern, targetPattern) {
44170
44237
  }
44171
44238
  return false;
44172
44239
  }
44240
+ function bashRuleMatchesCmd(rulePattern, serverId, cmd) {
44241
+ const rule = parsePattern(rulePattern);
44242
+ if (!rule)
44243
+ return false;
44244
+ if (!serverIdMatches(rule.serverId, serverId))
44245
+ return false;
44246
+ if (rule.toolName === "*" || rule.toolName === "BASH")
44247
+ return true;
44248
+ const m = rule.toolName.match(/^BASH\(cmd:\s*(.*)\)$/);
44249
+ if (!m)
44250
+ return false;
44251
+ let body = m[1];
44252
+ const isPrefix = body.endsWith(" *") || body.endsWith("*");
44253
+ if (isPrefix)
44254
+ body = body.replace(/\s*\*$/, "");
44255
+ const subs = splitShellCommands(cmd);
44256
+ if (subs.length === 0)
44257
+ return false;
44258
+ return subs.every((sub) => isPrefix ? sub === body || sub.startsWith(body + " ") : sub === body);
44259
+ }
44173
44260
  function isPatternInList(list, pattern) {
44174
44261
  if (!list)
44175
44262
  return false;
@@ -44181,6 +44268,28 @@ function isPatternAllowed(permissions, pattern) {
44181
44268
  function getNewPatterns(patterns, permissions) {
44182
44269
  return patterns.filter((p) => !isPatternAllowed(permissions, p));
44183
44270
  }
44271
+ function getBlockNewPatterns(block, permissions) {
44272
+ const allow = permissions?.allow ?? [];
44273
+ if (Array.isArray(block.generalized_pattern)) {
44274
+ return getNewPatterns(block.generalized_pattern, permissions);
44275
+ }
44276
+ const agentPattern = typeof block.generalized_pattern === "string" ? block.generalized_pattern : "";
44277
+ const isBash = (block.type.toLowerCase() === "bash" || agentPattern.includes(":BASH(")) && !!block.cmd;
44278
+ if (!isBash) {
44279
+ return getBlockPatterns(block).filter((p) => !isPatternAllowed(permissions, p));
44280
+ }
44281
+ const serverId = getBlockServerId(block);
44282
+ const prefix = serverId === "*" ? "" : `${serverId}:`;
44283
+ const rawSubs = splitShellCommands(block.cmd);
44284
+ const pairs = [];
44285
+ for (const raw of rawSubs) {
44286
+ const parsed = parseBashCmd(raw);
44287
+ if (parsed.length === 0)
44288
+ continue;
44289
+ pairs.push({ raw, pattern: prefix + buildBashPattern(parsed[0]) });
44290
+ }
44291
+ return pairs.filter(({ raw, pattern }) => !isPatternAllowed(permissions, pattern) && !allow.some((rule) => bashRuleMatchesCmd(rule, serverId, raw))).map(({ pattern }) => pattern);
44292
+ }
44184
44293
 
44185
44294
  // src/diff-view.ts
44186
44295
  var import_diff_match_patch = __toESM(require_diff_match_patch(), 1);
@@ -44428,12 +44537,14 @@ function renderDiff(originalContent, modifiedContent, filePath) {
44428
44537
  var diffStoreByWs = new WeakMap;
44429
44538
  function classifyBlock(info) {
44430
44539
  const inner = (info.block_type || "").toLowerCase();
44431
- if (["create", "createfile"].includes(inner))
44540
+ if (["create", "createfile", "write"].includes(inner))
44432
44541
  return "create";
44433
44542
  if (["modify", "modifyfile", "update", "edit"].includes(inner))
44434
44543
  return "edit";
44435
44544
  if (["catfile", "read", "readfile"].includes(inner))
44436
44545
  return "read";
44546
+ if (["search", "grep"].includes(inner))
44547
+ return "search";
44437
44548
  if (inner === "mcp")
44438
44549
  return "mcp";
44439
44550
  if (["shell", "bash"].includes(inner) || info.cmd)
@@ -44441,7 +44552,7 @@ function classifyBlock(info) {
44441
44552
  return "unknown";
44442
44553
  }
44443
44554
  function blockDisplay(info) {
44444
- const labels = { create: "File", edit: "Edit", read: "Read File", mcp: "MCP", shell: "Shell" };
44555
+ const labels = { create: "File", edit: "Edit", read: "Read File", search: "Search", mcp: "MCP", shell: "Shell" };
44445
44556
  const kind = classifyBlock(info);
44446
44557
  const typeLabel = labels[kind] || info.block_type || "Tool";
44447
44558
  const skipKeys = new Set([
@@ -44587,12 +44698,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
44587
44698
  process.stderr.write(renderDiff(diff.originalContent, diff.modifiedContent, filePath));
44588
44699
  }
44589
44700
  }
44590
- const allPatterns = blocks.flatMap((bi) => getBlockPatterns({
44701
+ const newPatterns = blocks.flatMap((bi) => getBlockNewPatterns({
44591
44702
  type: bi.block_type || "unknown",
44592
44703
  generalized_pattern: bi.generalized_pattern,
44593
44704
  cmd: bi.cmd
44594
- }));
44595
- const newPatterns = getNewPatterns(allPatterns, opts.agentSettings?.permissions);
44705
+ }, opts.agentSettings?.permissions));
44596
44706
  const stripPrefix = (p) => p.replace(/^todoai_(edge|cloud):/, "");
44597
44707
  const patternHint = newPatterns.length ? ` ${DIM}${newPatterns.map(stripPrefix).join(", ")}${RESET}` : "";
44598
44708
  try {
@@ -44605,11 +44715,11 @@ ${YELLOW}\u26A0 ${blocks.length} action(s) awaiting approval:${RESET}
44605
44715
  for (const bi of blocks) {
44606
44716
  let patterns;
44607
44717
  if (response === "r") {
44608
- patterns = getBlockPatterns({
44718
+ patterns = getBlockNewPatterns({
44609
44719
  type: bi.block_type || "unknown",
44610
44720
  generalized_pattern: bi.generalized_pattern,
44611
44721
  cmd: bi.cmd
44612
- });
44722
+ }, opts.agentSettings?.permissions);
44613
44723
  if (patterns.length > 0) {
44614
44724
  process.stderr.write(` ${GREEN}\u2713 Remembering: ${patterns.map(stripPrefix).join(", ")}${RESET}
44615
44725
  `);
@@ -44747,10 +44857,62 @@ ${DIM}[todo:status] ${status}${RESET}
44747
44857
  }
44748
44858
  }
44749
44859
 
44860
+ // src/list-agents.ts
44861
+ async function listAgentsCommand(api, opts) {
44862
+ const agents = await api.listAgentSettings();
44863
+ if (opts.json) {
44864
+ console.log(JSON.stringify(agents, null, 2));
44865
+ return;
44866
+ }
44867
+ if (!agents.length) {
44868
+ process.stderr.write(`No agents found.
44869
+ `);
44870
+ return;
44871
+ }
44872
+ const rows = agents.map((a) => ({
44873
+ name: getDisplayName(a),
44874
+ id: getItemId(a),
44875
+ model: a.model || "",
44876
+ paths: getAgentWorkspacePaths(a).map(opts.formatPath)
44877
+ }));
44878
+ const nameW = Math.max(4, ...rows.map((r) => r.name.length));
44879
+ const modelW = Math.max(5, ...rows.map((r) => r.model.length));
44880
+ process.stderr.write(`${DIM}${"NAME".padEnd(nameW)} ${"MODEL".padEnd(modelW)} ID${" ".repeat(34)}PATHS${RESET}
44881
+ `);
44882
+ for (const r of rows) {
44883
+ process.stderr.write(`${BRAND}${r.name.padEnd(nameW)}${RESET} ${CYAN}${r.model.padEnd(modelW)}${RESET} ${DIM}${r.id}${RESET} ${r.paths.join(", ")}
44884
+ `);
44885
+ }
44886
+ }
44887
+
44888
+ // src/ensure-edge.ts
44889
+ import { spawn } from "child_process";
44890
+ import fs from "fs";
44891
+ import path2 from "path";
44892
+ import os2 from "os";
44893
+ function ensureEdgeRunning(apiUrl, apiKey) {
44894
+ const logDir = path2.join(os2.homedir(), ".todoforai");
44895
+ fs.mkdirSync(logDir, { recursive: true });
44896
+ const logFile = path2.join(logDir, "edge.log");
44897
+ const out = fs.openSync(logFile, "a");
44898
+ const child = spawn("bunx", ["@todoforai/edge", "--api-url", apiUrl, "--api-key", apiKey], {
44899
+ detached: true,
44900
+ stdio: ["ignore", out, out]
44901
+ });
44902
+ child.unref();
44903
+ const pid = child.pid;
44904
+ setTimeout(() => {
44905
+ try {
44906
+ process.kill(pid, 0);
44907
+ console.error(`\x1B[2mStarted edge daemon (pid ${pid}), logs: ${logFile.replace(os2.homedir(), "~")}\x1B[0m`);
44908
+ } catch {}
44909
+ }, 500);
44910
+ }
44911
+
44750
44912
  // src/index.ts
44751
- function formatPathWithTilde(path2) {
44752
- const home = homedir2();
44753
- return path2.startsWith(home) ? path2.replace(home, "~") : path2;
44913
+ function formatPathWithTilde(path3) {
44914
+ const home = homedir3();
44915
+ return path3.startsWith(home) ? path3.replace(home, "~") : path3;
44754
44916
  }
44755
44917
  function getFrontendUrl(apiUrl, projectId, todoId) {
44756
44918
  if (apiUrl.includes("localhost:4000") || apiUrl.includes("127.0.0.1:4000")) {
@@ -44833,6 +44995,10 @@ Cancelled by user (Ctrl+C)
44833
44995
  process.exit(130);
44834
44996
  });
44835
44997
  const { values: args, positionals } = parseCliArgs();
44998
+ if (args.version) {
44999
+ console.log(VERSION);
45000
+ process.exit(0);
45001
+ }
44836
45002
  if (args.help) {
44837
45003
  printUsage();
44838
45004
  process.exit(0);
@@ -44844,8 +45010,8 @@ Cancelled by user (Ctrl+C)
44844
45010
  return;
44845
45011
  }
44846
45012
  if (args["reset-config"]) {
44847
- const { existsSync: existsSync2, unlinkSync } = await import("fs");
44848
- if (existsSync2(cfg.path)) {
45013
+ const { existsSync: existsSync3, unlinkSync } = await import("fs");
45014
+ if (existsSync3(cfg.path)) {
44849
45015
  unlinkSync(cfg.path);
44850
45016
  console.log(`Configuration reset: ${formatPathWithTilde(cfg.path)}`);
44851
45017
  } else
@@ -44857,21 +45023,6 @@ Cancelled by user (Ctrl+C)
44857
45023
  console.log(`Default API URL set to: ${args["set-default-api-url"]}`);
44858
45024
  return;
44859
45025
  }
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
45026
  const apiUrl = normalizeApiUrl(args["api-url"] || cfg.data.default_api_url || getEnv("API_URL") || DEFAULT_API_URL);
44876
45027
  async function deviceLogin() {
44877
45028
  const loginApi = new ApiClient(apiUrl, "");
@@ -44886,12 +45037,12 @@ Cancelled by user (Ctrl+C)
44886
45037
 
44887
45038
  `);
44888
45039
  try {
44889
- const { spawn } = await import("child_process");
45040
+ const { spawn: spawn2 } = await import("child_process");
44890
45041
  if (process.platform === "win32") {
44891
- spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
45042
+ spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
44892
45043
  } else {
44893
45044
  const cmd = process.platform === "darwin" ? "open" : "xdg-open";
44894
- spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
45045
+ spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
44895
45046
  }
44896
45047
  } catch {}
44897
45048
  process.stderr.write(`Waiting for approval (expires in ${Math.round(expiresIn / 60)}min)...
@@ -44904,7 +45055,7 @@ Cancelled by user (Ctrl+C)
44904
45055
  const poll = await loginApi.pollDeviceLogin(code);
44905
45056
  failures = 0;
44906
45057
  if (poll.status === "complete" && poll.apiKey) {
44907
- cfg.setDefaultApiKey(poll.apiKey);
45058
+ writeCredential(apiUrl, poll.apiKey);
44908
45059
  process.stderr.write(`${GREEN}\u2705 Login successful! API key saved.${RESET}
44909
45060
  `);
44910
45061
  return poll.apiKey;
@@ -44927,11 +45078,15 @@ Cancelled by user (Ctrl+C)
44927
45078
  await deviceLogin();
44928
45079
  return;
44929
45080
  }
44930
- let apiKey = args["api-key"] || cfg.data.default_api_key || getEnv("API_KEY") || "";
45081
+ let apiKey = args["api-key"] || getEnv("API_KEY") || readCredential(apiUrl) || "";
44931
45082
  if (!apiKey) {
44932
45083
  apiKey = await deviceLogin();
44933
45084
  }
44934
45085
  const api = new ApiClient(apiUrl, apiKey);
45086
+ if (args["list-agents"]) {
45087
+ await listAgentsCommand(api, { json: !!args.json, formatPath: formatPathWithTilde });
45088
+ return;
45089
+ }
44935
45090
  if (args.inspect) {
44936
45091
  const todoId = args.inspect;
44937
45092
  const todo2 = await api.getTodo(todoId);
@@ -44941,6 +45096,8 @@ Cancelled by user (Ctrl+C)
44941
45096
  if (process.stderr.isTTY)
44942
45097
  printLogo();
44943
45098
  if (args.template) {
45099
+ if (!args["no-edge"])
45100
+ ensureEdgeRunning(apiUrl, apiKey);
44944
45101
  const templateId = args.template;
44945
45102
  const inputValues = {};
44946
45103
  for (const kv of args.input || []) {
@@ -45028,6 +45185,8 @@ ${"\u2500".repeat(40)}
45028
45185
  `);
45029
45186
  }
45030
45187
  if (args.resume || args.continue) {
45188
+ if (!args["no-edge"])
45189
+ ensureEdgeRunning(apiUrl, apiKey);
45031
45190
  const todoId = args.resume || cfg.data.last_todo_id;
45032
45191
  if (!todoId) {
45033
45192
  process.stderr.write(`Error: No recent todo found
@@ -45095,6 +45254,8 @@ Resumed todo: ${todoId}
45095
45254
  }
45096
45255
  process.stderr.write(`${DIM}Tip: ${randomTip()}${RESET}
45097
45256
  `);
45257
+ if (!args["no-edge"])
45258
+ ensureEdgeRunning(apiUrl, apiKey);
45098
45259
  let content;
45099
45260
  if (positionals.length > 0) {
45100
45261
  content = positionals.join(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoai": "dist/todoai.js"