@todoforai/cli 0.1.5 → 0.1.7

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 +3 -6
  2. package/dist/todoai.js +130 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,13 +17,11 @@ todoai # prompts device login if no key found
17
17
  todoai login # explicit login
18
18
  ```
19
19
 
20
- Optional manual config:
20
+ API URL resolution: `--api-url` flag → `TODOFORAI_API_URL` env → `https://api.todofor.ai`.
21
21
 
22
- ```bash
23
- todoai --set-default-api-url http://localhost:4000 # or https://api.todofor.ai
24
- ```
22
+ Auth resolution: `--api-key` flag → `TODOFORAI_API_KEY` env → shared credentials (`~/.todoforai/credentials.json`) → device login.
25
23
 
26
- Auth resolution order: `--api-key` flag `TODOFORAI_API_KEY` env shared credentials (`~/.todoforai/credentials.json`) device login.
24
+ 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.
27
25
 
28
26
  ## Edge daemon
29
27
 
@@ -89,7 +87,6 @@ todoai --resume <todo-id> # resume specific todo
89
87
  --safe Validate API key upfront
90
88
  --debug, -d Debug output
91
89
  --show-config Show config
92
- --set-default-api-url Set default API URL
93
90
  --reset-config Reset config file
94
91
  --help, -h Show this help
95
92
  ```
package/dist/todoai.js CHANGED
@@ -42776,6 +42776,12 @@ class ApiClient {
42776
42776
  updateAgentSettings(agentId, agentSettingsId, updates) {
42777
42777
  return this.request("PUT", `/api/v1/agents/${agentId}/settings`, { agentSettingsId, updates });
42778
42778
  }
42779
+ getGlobalAgentSettings() {
42780
+ return this.request("GET", "/api/v1/agents/global");
42781
+ }
42782
+ updateGlobalAgentSettings(updates) {
42783
+ return this.request("PUT", "/api/v1/agents/global", updates);
42784
+ }
42779
42785
  setAgentEdgeMcpConfig(agentId, agentSettingsId, edgeId, mcpName, config) {
42780
42786
  return this.request("PUT", `/api/v1/agents/${agentId}/edge-mcp-config`, { agentSettingsId, edgeId, mcpName, config });
42781
42787
  }
@@ -43107,7 +43113,6 @@ Options:
43107
43113
  --safe Validate API key upfront
43108
43114
  --debug, -d Debug output
43109
43115
  --show-config Show config
43110
- --set-default-api-url Set default API URL
43111
43116
  --reset-config Reset config file
43112
43117
  --version, -v Print version and exit
43113
43118
  --help, -h Show this help
@@ -43137,7 +43142,6 @@ function parseCliArgs() {
43137
43142
  safe: { type: "boolean", default: false },
43138
43143
  debug: { type: "boolean", short: "d", default: false },
43139
43144
  "show-config": { type: "boolean", default: false },
43140
- "set-default-api-url": { type: "string" },
43141
43145
  "reset-config": { type: "boolean", default: false },
43142
43146
  "config-path": { type: "string" },
43143
43147
  help: { type: "boolean", short: "h", default: false },
@@ -43535,20 +43539,50 @@ function getConfigDir() {
43535
43539
  const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
43536
43540
  return join(xdg, "todoai-cli");
43537
43541
  }
43538
- function defaultConfig() {
43542
+ function defaultScope() {
43539
43543
  return {
43540
43544
  default_project_id: null,
43541
43545
  default_project_name: null,
43542
43546
  default_agent_name: null,
43543
43547
  default_agent_settings: null,
43544
43548
  default_agent_settings_updated_at: null,
43545
- default_api_url: null,
43546
43549
  recent_projects: [],
43547
43550
  recent_agents: [],
43548
- last_todo_id: null,
43549
- input_history: []
43551
+ last_todo_id: null
43550
43552
  };
43551
43553
  }
43554
+ function defaultConfig() {
43555
+ return {
43556
+ input_history: [],
43557
+ per_api_url: {}
43558
+ };
43559
+ }
43560
+ function migrate(raw) {
43561
+ const cfg = { ...defaultConfig(), ...raw, per_api_url: raw.per_api_url || {} };
43562
+ const legacyKeys = [
43563
+ "default_project_id",
43564
+ "default_project_name",
43565
+ "default_agent_name",
43566
+ "default_agent_settings",
43567
+ "default_agent_settings_updated_at",
43568
+ "recent_projects",
43569
+ "recent_agents",
43570
+ "last_todo_id"
43571
+ ];
43572
+ const hasLegacy = legacyKeys.some((k) => raw[k] != null);
43573
+ if (hasLegacy) {
43574
+ const url = raw.default_api_url || "https://api.todofor.ai";
43575
+ const scope = { ...defaultScope(), ...cfg.per_api_url[url] || {} };
43576
+ for (const k of legacyKeys)
43577
+ if (raw[k] != null)
43578
+ scope[k] = raw[k];
43579
+ cfg.per_api_url[url] = scope;
43580
+ }
43581
+ for (const k of legacyKeys)
43582
+ delete cfg[k];
43583
+ delete cfg.default_api_url;
43584
+ return cfg;
43585
+ }
43552
43586
 
43553
43587
  class ConfigStore {
43554
43588
  path;
@@ -43568,7 +43602,13 @@ class ConfigStore {
43568
43602
  try {
43569
43603
  const raw = JSON.parse(readFileSync(this.path, "utf-8"));
43570
43604
  delete raw.default_api_key;
43571
- return { ...defaultConfig(), ...raw };
43605
+ const cfg = migrate(raw);
43606
+ const needsMigration = raw.per_api_url == null;
43607
+ if (needsMigration) {
43608
+ this.data = cfg;
43609
+ this.save();
43610
+ }
43611
+ return cfg;
43572
43612
  } catch {
43573
43613
  return defaultConfig();
43574
43614
  }
@@ -43579,26 +43619,10 @@ class ConfigStore {
43579
43619
  writeFileSync(this.path, JSON.stringify(this.data, null, 2), "utf-8");
43580
43620
  } catch {}
43581
43621
  }
43582
- setDefaultProject(id, name) {
43583
- this.data.default_project_id = id;
43584
- this.data.default_project_name = name || id;
43585
- const recent = this.data.recent_projects.filter((p) => p.id !== id);
43586
- recent.unshift({ id, name: name || id });
43587
- this.data.recent_projects = recent.slice(0, 10);
43588
- this.save();
43589
- }
43590
- setDefaultAgent(name, settings) {
43591
- this.data.default_agent_name = name;
43592
- this.data.default_agent_settings = settings || null;
43593
- this.data.default_agent_settings_updated_at = new Date().toISOString();
43594
- const recent = this.data.recent_agents.filter((a) => a !== name);
43595
- recent.unshift(name);
43596
- this.data.recent_agents = recent.slice(0, 10);
43597
- this.save();
43598
- }
43599
- setDefaultApiUrl(url) {
43600
- this.data.default_api_url = url;
43601
- this.save();
43622
+ scope(apiUrl) {
43623
+ if (!this.data.per_api_url[apiUrl])
43624
+ this.data.per_api_url[apiUrl] = defaultScope();
43625
+ return new ScopedConfig(this, apiUrl);
43602
43626
  }
43603
43627
  addToHistory(input) {
43604
43628
  const trimmed = input.trim();
@@ -43616,6 +43640,37 @@ class ConfigStore {
43616
43640
  }
43617
43641
  }
43618
43642
 
43643
+ class ScopedConfig {
43644
+ store;
43645
+ apiUrl;
43646
+ constructor(store, apiUrl) {
43647
+ this.store = store;
43648
+ this.apiUrl = apiUrl;
43649
+ }
43650
+ get data() {
43651
+ return this.store.data.per_api_url[this.apiUrl];
43652
+ }
43653
+ setDefaultProject(id, name) {
43654
+ const s = this.data;
43655
+ s.default_project_id = id;
43656
+ s.default_project_name = name || id;
43657
+ s.recent_projects = [{ id, name: name || id }, ...s.recent_projects.filter((p) => p.id !== id)].slice(0, 10);
43658
+ this.store.save();
43659
+ }
43660
+ setDefaultAgent(name, settings) {
43661
+ const s = this.data;
43662
+ s.default_agent_name = name;
43663
+ s.default_agent_settings = settings || null;
43664
+ s.default_agent_settings_updated_at = new Date().toISOString();
43665
+ s.recent_agents = [name, ...s.recent_agents.filter((a) => a !== name)].slice(0, 10);
43666
+ this.store.save();
43667
+ }
43668
+ setLastTodoId(id) {
43669
+ this.data.last_todo_id = id;
43670
+ this.store.save();
43671
+ }
43672
+ }
43673
+
43619
43674
  // src/credentials.ts
43620
43675
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
43621
43676
  import { dirname as dirname2, join as join2 } from "path";
@@ -44886,11 +44941,19 @@ async function listAgentsCommand(api, opts) {
44886
44941
  }
44887
44942
 
44888
44943
  // src/ensure-edge.ts
44889
- import { spawn } from "child_process";
44944
+ import { spawn, spawnSync } from "child_process";
44890
44945
  import fs from "fs";
44891
44946
  import path2 from "path";
44892
44947
  import os2 from "os";
44948
+ function hasBunx() {
44949
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bunx"], { stdio: "ignore" });
44950
+ return probe.status === 0;
44951
+ }
44893
44952
  function ensureEdgeRunning(apiUrl, apiKey) {
44953
+ if (!hasBunx()) {
44954
+ console.error("\x1B[2mEdge daemon not started: `bunx` is missing. Install Bun from https://bun.sh to enable it, or pass --no-edge to silence this.\x1B[0m");
44955
+ return;
44956
+ }
44894
44957
  const logDir = path2.join(os2.homedir(), ".todoforai");
44895
44958
  fs.mkdirSync(logDir, { recursive: true });
44896
44959
  const logFile = path2.join(logDir, "edge.log");
@@ -44899,13 +44962,30 @@ function ensureEdgeRunning(apiUrl, apiKey) {
44899
44962
  detached: true,
44900
44963
  stdio: ["ignore", out, out]
44901
44964
  });
44965
+ child.on("error", (err) => {
44966
+ console.error(`\x1B[33mFailed to start edge daemon: ${err.message}\x1B[0m`);
44967
+ });
44968
+ let exited = false;
44969
+ let exitCode = null;
44970
+ child.on("exit", (code) => {
44971
+ exited = true;
44972
+ exitCode = code;
44973
+ });
44902
44974
  child.unref();
44903
44975
  const pid = child.pid;
44976
+ if (!pid)
44977
+ return;
44978
+ const shortLog = logFile.replace(os2.homedir(), "~");
44904
44979
  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 {}
44980
+ if (!exited) {
44981
+ console.error(`\x1B[2mStarted edge daemon (pid ${pid}), logs: ${shortLog}\x1B[0m`);
44982
+ return;
44983
+ }
44984
+ if (exitCode === 0) {
44985
+ console.error(`\x1B[2mEdge daemon exited cleanly (another instance likely already running). Logs: ${shortLog}\x1B[0m`);
44986
+ } else {
44987
+ console.error(`\x1B[31mEdge daemon died (exit ${exitCode}). Check logs: ${shortLog}\x1B[0m`);
44988
+ }
44909
44989
  }, 500);
44910
44990
  }
44911
44991
 
@@ -45018,12 +45098,8 @@ Cancelled by user (Ctrl+C)
45018
45098
  console.log("No configuration file to reset");
45019
45099
  return;
45020
45100
  }
45021
- if (args["set-default-api-url"]) {
45022
- cfg.setDefaultApiUrl(args["set-default-api-url"]);
45023
- console.log(`Default API URL set to: ${args["set-default-api-url"]}`);
45024
- return;
45025
- }
45026
- const apiUrl = normalizeApiUrl(args["api-url"] || cfg.data.default_api_url || getEnv("API_URL") || DEFAULT_API_URL);
45101
+ const apiUrl = normalizeApiUrl(args["api-url"] || getEnv("API_URL") || DEFAULT_API_URL);
45102
+ const cfgScope = cfg.scope(apiUrl);
45027
45103
  async function deviceLogin() {
45028
45104
  const loginApi = new ApiClient(apiUrl, "");
45029
45105
  const { code, url, expiresIn } = await loginApi.initDeviceLogin("cli");
@@ -45096,7 +45172,7 @@ Cancelled by user (Ctrl+C)
45096
45172
  if (process.stderr.isTTY)
45097
45173
  printLogo();
45098
45174
  if (args.template) {
45099
- if (!args["no-edge"])
45175
+ if (!args["no-edge"] && !args["no-watch"])
45100
45176
  ensureEdgeRunning(apiUrl, apiKey);
45101
45177
  const templateId = args.template;
45102
45178
  const inputValues = {};
@@ -45132,7 +45208,7 @@ Cancelled by user (Ctrl+C)
45132
45208
  const projects2 = await api.listProjects();
45133
45209
  let projectId2 = args.project;
45134
45210
  if (!projectId2) {
45135
- projectId2 = cfg.data.default_project_id || projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
45211
+ projectId2 = cfgScope.data.default_project_id || projects2.find((p) => p.project?.isDefault)?.project?.id || projects2[0]?.project?.id;
45136
45212
  }
45137
45213
  if (!projectId2) {
45138
45214
  process.stderr.write(`Error: No project found
@@ -45141,8 +45217,7 @@ Cancelled by user (Ctrl+C)
45141
45217
  }
45142
45218
  const todo2 = await api.startFromTemplate(projectId2, templateId, { inputValues });
45143
45219
  const todoId = todo2.id;
45144
- cfg.data.last_todo_id = todoId;
45145
- cfg.save();
45220
+ cfgScope.setLastTodoId(todoId);
45146
45221
  const frontendUrl2 = getFrontendUrl(apiUrl, projectId2, todoId);
45147
45222
  if (args.json) {
45148
45223
  console.log(JSON.stringify({ ...todo2, frontend_url: frontendUrl2 }, null, 2));
@@ -45187,7 +45262,7 @@ ${"\u2500".repeat(40)}
45187
45262
  if (args.resume || args.continue) {
45188
45263
  if (!args["no-edge"])
45189
45264
  ensureEdgeRunning(apiUrl, apiKey);
45190
- const todoId = args.resume || cfg.data.last_todo_id;
45265
+ const todoId = args.resume || cfgScope.data.last_todo_id;
45191
45266
  if (!todoId) {
45192
45267
  process.stderr.write(`Error: No recent todo found
45193
45268
  `);
@@ -45222,20 +45297,20 @@ Resumed todo: ${todoId}
45222
45297
  `);
45223
45298
  process.exit(1);
45224
45299
  }
45225
- cfg.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45300
+ cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45226
45301
  } else {
45227
45302
  const pathArg = args.path || ".";
45228
45303
  const resolved = realpathSync(resolve3(pathArg));
45229
45304
  const matches = await api.listAgentSettings({ workspacePath: resolved });
45230
45305
  if (matches.length > 0) {
45231
45306
  preMatchedAgent = matches[0];
45232
- cfg.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45307
+ cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45233
45308
  } else if (args.path) {
45234
45309
  process.stderr.write(`No agent found for '${formatPathWithTilde(resolved)}', creating one...
45235
45310
  `);
45236
45311
  try {
45237
45312
  preMatchedAgent = await autoCreateAgent(api, resolved);
45238
- cfg.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45313
+ cfgScope.setDefaultAgent(getDisplayName(preMatchedAgent), preMatchedAgent);
45239
45314
  } catch (e) {
45240
45315
  process.stderr.write(`Error: Failed to auto-create agent: ${e.message}
45241
45316
  `);
@@ -45254,7 +45329,7 @@ Resumed todo: ${todoId}
45254
45329
  }
45255
45330
  process.stderr.write(`${DIM}Tip: ${randomTip()}${RESET}
45256
45331
  `);
45257
- if (!args["no-edge"])
45332
+ if (!args["no-edge"] && !args["no-watch"])
45258
45333
  ensureEdgeRunning(apiUrl, apiKey);
45259
45334
  let content;
45260
45335
  if (positionals.length > 0) {
@@ -45262,8 +45337,8 @@ Resumed todo: ${todoId}
45262
45337
  } else {
45263
45338
  content = await readStdin();
45264
45339
  }
45265
- const hasProject = args.project || cfg.data.default_project_id;
45266
- const storedAgent = cfg.data.default_agent_settings;
45340
+ const hasProject = args.project || cfgScope.data.default_project_id;
45341
+ const storedAgent = cfgScope.data.default_agent_settings;
45267
45342
  const hasAgent = preMatchedAgent || storedAgent?.id && !args.agent;
45268
45343
  let projects = null;
45269
45344
  if (!hasProject || !hasAgent || args.safe || args.debug) {
@@ -45281,11 +45356,11 @@ Resumed todo: ${todoId}
45281
45356
  if (match)
45282
45357
  projectName = getDisplayName(match);
45283
45358
  }
45284
- } else if (cfg.data.default_project_id && !projects) {
45285
- projectId = cfg.data.default_project_id;
45286
- projectName = cfg.data.default_project_name || projectId;
45359
+ } else if (cfgScope.data.default_project_id && !projects) {
45360
+ projectId = cfgScope.data.default_project_id;
45361
+ projectName = cfgScope.data.default_project_name || projectId;
45287
45362
  } else {
45288
- const sel = await selectProject(projects, cfg.data.default_project_id, (id, name) => cfg.setDefaultProject(id, name));
45363
+ const sel = await selectProject(projects, cfgScope.data.default_project_id, (id, name) => cfgScope.setDefaultProject(id, name));
45289
45364
  projectId = sel.id;
45290
45365
  projectName = sel.name;
45291
45366
  }
@@ -45295,7 +45370,7 @@ Resumed todo: ${todoId}
45295
45370
  } else if (storedAgent?.id && !agents) {
45296
45371
  agent = storedAgent;
45297
45372
  } else {
45298
- agent = await selectAgent(agents, cfg.data.default_agent_name, (name, settings) => cfg.setDefaultAgent(name, settings));
45373
+ agent = await selectAgent(agents, cfgScope.data.default_agent_name, (name, settings) => cfgScope.setDefaultAgent(name, settings));
45299
45374
  }
45300
45375
  const ws = args["no-watch"] ? null : new FrontendWebSocket(apiUrl, apiKey);
45301
45376
  if (ws)
@@ -45309,8 +45384,7 @@ Resumed todo: ${todoId}
45309
45384
  cfg.addToHistory(content);
45310
45385
  const todo = await api.addMessage(projectId, content, agent);
45311
45386
  const actualTodoId = todo.id || crypto.randomUUID();
45312
- cfg.data.last_todo_id = actualTodoId;
45313
- cfg.save();
45387
+ cfgScope.setLastTodoId(actualTodoId);
45314
45388
  const frontendUrl = getFrontendUrl(apiUrl, projectId, actualTodoId);
45315
45389
  if (args.json) {
45316
45390
  console.log(JSON.stringify({ ...todo, frontend_url: frontendUrl }, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoai": "dist/todoai.js"