@devstationlabs/cli 0.1.1 → 0.1.2

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 +13 -5
  2. package/devstation.js +556 -116
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,16 +27,24 @@ Either way, check the install:
27
27
  devstation doctor
28
28
  ```
29
29
 
30
- ## A key
30
+ ## Set up a model
31
31
 
32
- The agent needs a model. Set one of these and `doctor` will go green:
32
+ ```sh
33
+ devstation login
34
+ ```
35
+
36
+ Pick a provider, paste a key (it is hidden as you type), choose a model. It is
37
+ stored in `~/.devstation/credentials.json`, readable only by you, so every new
38
+ terminal just works. Anthropic, OpenRouter, OpenAI, and any OpenAI-compatible
39
+ server — Ollama, LM Studio, Groq — are supported:
33
40
 
34
41
  ```sh
35
- export ANTHROPIC_API_KEY=... # preferred: prompt caching, native tool use
36
- export OPENROUTER_API_KEY=... # also works
42
+ devstation login openai # then give it http://localhost:11434/v1 for Ollama
43
+ devstation config # what it will use, and where each value came from
37
44
  ```
38
45
 
39
- Put it in your shell profile so it survives a new terminal.
46
+ Exporting `ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY` still works, and is what a
47
+ server or CI job should do.
40
48
 
41
49
  ## Use it
42
50
 
package/devstation.js CHANGED
@@ -12504,12 +12504,13 @@ var init_sdk = __esm(() => {
12504
12504
 
12505
12505
  // src/lib/agent/cli/index.ts
12506
12506
  import { createInterface as createInterface2 } from "readline/promises";
12507
+ import { Writable } from "stream";
12507
12508
  import { resolve as resolve4 } from "path";
12508
- import { existsSync as existsSync11 } from "fs";
12509
+ import { existsSync as existsSync12 } from "fs";
12509
12510
 
12510
12511
  // src/lib/agent/cli/args.ts
12511
12512
  var CLI_NAME = "devstation";
12512
- var VERSION = "0.1.1";
12513
+ var VERSION = "0.1.2";
12513
12514
  var COMMANDS = new Set([
12514
12515
  "chat",
12515
12516
  "run",
@@ -12525,6 +12526,8 @@ var COMMANDS = new Set([
12525
12526
  "diff",
12526
12527
  "tools",
12527
12528
  "config",
12529
+ "login",
12530
+ "logout",
12528
12531
  "doctor",
12529
12532
  "version",
12530
12533
  "help"
@@ -12537,6 +12540,8 @@ var OFFLINE_COMMANDS = new Set([
12537
12540
  "diff",
12538
12541
  "tools",
12539
12542
  "config",
12543
+ "login",
12544
+ "logout",
12540
12545
  "doctor",
12541
12546
  "version",
12542
12547
  "help",
@@ -12552,6 +12557,7 @@ function parseArgs(argv, cwd = process.cwd()) {
12552
12557
  yes: false,
12553
12558
  json: false,
12554
12559
  sandbox: (process.env.DEVSTATION_SANDBOX ?? "").toLowerCase() !== "off",
12560
+ project: false,
12555
12561
  root: cwd
12556
12562
  };
12557
12563
  const words = [];
@@ -12576,6 +12582,9 @@ function parseArgs(argv, cwd = process.cwd()) {
12576
12582
  case "--no-sandbox":
12577
12583
  parsed.sandbox = false;
12578
12584
  break;
12585
+ case "--project":
12586
+ parsed.project = true;
12587
+ break;
12579
12588
  case "-h":
12580
12589
  case "--help":
12581
12590
  parsed.command = "help";
@@ -12664,7 +12673,12 @@ var HELP = `DevStation, the coding agent.
12664
12673
  ${CLI_NAME} memory show what it has been told about this project
12665
12674
  ${CLI_NAME} mcp the MCP servers configured here, and their tools
12666
12675
  ${CLI_NAME} tools list the tools it can use, and which ones ask first
12667
- ${CLI_NAME} config show the settings this run would use
12676
+ ${CLI_NAME} login [provider] store an API key and choose a model
12677
+ ${CLI_NAME} logout [provider] remove stored API keys
12678
+ ${CLI_NAME} config show the settings a run would use, and where each came from
12679
+ ${CLI_NAME} config set <key> <value> [--project]
12680
+ set provider, model or baseUrl
12681
+ ${CLI_NAME} config get|unset <key>, config path
12668
12682
  ${CLI_NAME} doctor check this machine is set up to run it
12669
12683
  ${CLI_NAME} version print the version
12670
12684
  ${CLI_NAME} help this
@@ -12681,12 +12695,14 @@ Options
12681
12695
  -f, --follow keep watching (status only)
12682
12696
  --json JSON from config, sessions, checkpoints and tools
12683
12697
  --no-sandbox run commands on this machine instead of in a container
12698
+ --project with config set/unset: write this workspace's config, not the global one
12684
12699
  -h, --help this
12685
12700
  -v, --version the version
12686
12701
 
12687
12702
  The repo command also needs GITHUB_TOKEN, with permission to push to that repository.
12688
12703
 
12689
- Set ANTHROPIC_API_KEY, or OPENROUTER_API_KEY, before running.
12704
+ Set up a model with \`${CLI_NAME} login\`, or export ANTHROPIC_API_KEY or OPENROUTER_API_KEY.
12705
+ Settings live in ~/.devstation/config.json, keys in ~/.devstation/credentials.json.
12690
12706
  `;
12691
12707
  var SESSION_HELP = ` /undo rewind the last checkpoint
12692
12708
  /status what this session has done so far
@@ -12704,7 +12720,7 @@ var SESSION_HELP = ` /undo rewind the last checkpoint
12704
12720
  `;
12705
12721
 
12706
12722
  // src/lib/agent/cli/commands.ts
12707
- import { accessSync, constants as constants2, existsSync as existsSync9 } from "fs";
12723
+ import { accessSync, constants as constants2, existsSync as existsSync10 } from "fs";
12708
12724
 
12709
12725
  // src/lib/agent/git.ts
12710
12726
  import { existsSync } from "fs";
@@ -17877,7 +17893,10 @@ class AnthropicProvider {
17877
17893
  model;
17878
17894
  client;
17879
17895
  constructor(opts = {}) {
17880
- this.client = new Anthropic(opts.apiKey ? { apiKey: opts.apiKey } : {});
17896
+ this.client = new Anthropic({
17897
+ ...opts.apiKey ? { apiKey: opts.apiKey } : {},
17898
+ ...opts.baseUrl ? { baseURL: opts.baseUrl } : {}
17899
+ });
17881
17900
  this.model = opts.model || DEFAULT_MODEL;
17882
17901
  }
17883
17902
  async generate(input) {
@@ -17929,7 +17948,7 @@ class AnthropicProvider {
17929
17948
  }
17930
17949
 
17931
17950
  // src/lib/agent/providers/openrouter.ts
17932
- var ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
17951
+ var OPENROUTER_BASE = "https://openrouter.ai/api/v1";
17933
17952
  function stopReasonOf2(raw) {
17934
17953
  switch (raw) {
17935
17954
  case "stop":
@@ -17980,20 +17999,24 @@ function parseArguments(raw) {
17980
17999
 
17981
18000
  class OpenRouterProvider {
17982
18001
  apiKey;
17983
- name = "openrouter";
18002
+ name;
17984
18003
  model;
17985
- constructor(apiKey, model) {
18004
+ endpoint;
18005
+ constructor(apiKey, model, opts = {}) {
17986
18006
  this.apiKey = apiKey;
17987
- this.model = model || process.env.AI_MODEL || "anthropic/claude-sonnet-5";
18007
+ this.name = opts.name ?? "openrouter";
18008
+ const base = (opts.baseUrl || OPENROUTER_BASE).replace(/\/+$/, "");
18009
+ this.endpoint = `${base}/chat/completions`;
18010
+ this.model = model || process.env.AI_MODEL || (this.name === "openrouter" ? "anthropic/claude-sonnet-5" : "");
17988
18011
  }
17989
18012
  async generate(input) {
17990
- const res = await fetch(ENDPOINT, {
18013
+ const openRouter = this.name === "openrouter";
18014
+ const res = await fetch(this.endpoint, {
17991
18015
  method: "POST",
17992
18016
  headers: {
17993
18017
  "content-type": "application/json",
17994
- authorization: `Bearer ${this.apiKey}`,
17995
- "HTTP-Referer": "https://devstation.online",
17996
- "X-Title": "DevStation"
18018
+ ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {},
18019
+ ...openRouter ? { "HTTP-Referer": "https://devstation.online", "X-Title": "DevStation" } : {}
17997
18020
  },
17998
18021
  body: JSON.stringify({
17999
18022
  model: this.model,
@@ -18011,13 +18034,14 @@ class OpenRouterProvider {
18011
18034
  }
18012
18035
  }))
18013
18036
  } : {},
18014
- ...process.env.AI_REASONING === "on" ? {} : { reasoning: { enabled: false } }
18037
+ ...openRouter && process.env.AI_REASONING !== "on" ? { reasoning: { enabled: false } } : {}
18015
18038
  }),
18016
18039
  signal: input.signal
18017
18040
  });
18018
18041
  if (!res.ok || !res.body) {
18019
18042
  const detail = await res.text().catch(() => "");
18020
- throw new Error(`OpenRouter request failed (${res.status}). ${detail.slice(0, 200)}`);
18043
+ const label = openRouter ? "OpenRouter" : `The endpoint ${this.endpoint}`;
18044
+ throw new Error(`${label} request failed (${res.status}). ${detail.slice(0, 200)}`);
18021
18045
  }
18022
18046
  let text = "";
18023
18047
  let finish = null;
@@ -18085,21 +18109,245 @@ class OpenRouterProvider {
18085
18109
  };
18086
18110
  }
18087
18111
  }
18088
- // src/lib/agent/providers/index.ts
18089
- function providerFromEnv(env2 = process.env, model) {
18090
- if (env2.ANTHROPIC_API_KEY)
18091
- return new AnthropicProvider({ apiKey: env2.ANTHROPIC_API_KEY, model });
18092
- const openRouter = env2.OPENROUTER_API_KEY || env2.AI_API_KEY;
18093
- if (openRouter)
18094
- return new OpenRouterProvider(openRouter, model);
18095
- return null;
18112
+ // src/lib/agent/providers/settings.ts
18113
+ import {
18114
+ chmodSync,
18115
+ existsSync as existsSync4,
18116
+ mkdirSync as mkdirSync3,
18117
+ readFileSync as readFileSync3,
18118
+ renameSync,
18119
+ statSync as statSync2,
18120
+ writeFileSync as writeFileSync3
18121
+ } from "fs";
18122
+ import { dirname as dirname4, join as join5 } from "path";
18123
+ var PROVIDER_IDS = ["anthropic", "openrouter", "openai"];
18124
+ var SETTING_KEYS = ["provider", "model", "baseUrl"];
18125
+ var KEY_ENV = {
18126
+ anthropic: ["ANTHROPIC_API_KEY"],
18127
+ openrouter: ["OPENROUTER_API_KEY", "AI_API_KEY"],
18128
+ openai: ["OPENAI_API_KEY"]
18129
+ };
18130
+ var DEFAULT_BASE_URL = {
18131
+ anthropic: "https://api.anthropic.com",
18132
+ openrouter: "https://openrouter.ai/api/v1",
18133
+ openai: "https://api.openai.com/v1"
18134
+ };
18135
+ function globalDir(home) {
18136
+ return join5(home, ".devstation");
18096
18137
  }
18097
- function configuredProviderName(env2 = process.env) {
18098
- if (env2.ANTHROPIC_API_KEY)
18099
- return "anthropic";
18100
- if (env2.OPENROUTER_API_KEY || env2.AI_API_KEY)
18101
- return "openrouter";
18102
- return null;
18138
+ function globalConfigPath(home) {
18139
+ return join5(globalDir(home), "config.json");
18140
+ }
18141
+ function projectConfigPath(root) {
18142
+ return join5(root, ".devstation", "config.json");
18143
+ }
18144
+ function credentialsPath(home) {
18145
+ return join5(globalDir(home), "credentials.json");
18146
+ }
18147
+ function isProvider(value) {
18148
+ return typeof value === "string" && PROVIDER_IDS.includes(value);
18149
+ }
18150
+ function readSettingsFile(path4, problems = []) {
18151
+ if (!existsSync4(path4))
18152
+ return {};
18153
+ try {
18154
+ const raw = JSON.parse(readFileSync3(path4, "utf8"));
18155
+ const out = {};
18156
+ if (raw.provider !== undefined) {
18157
+ if (isProvider(raw.provider))
18158
+ out.provider = raw.provider;
18159
+ else
18160
+ problems.push(`${path4}: unknown provider "${String(raw.provider)}".`);
18161
+ }
18162
+ if (typeof raw.model === "string" && raw.model.trim())
18163
+ out.model = raw.model.trim();
18164
+ if (typeof raw.baseUrl === "string" && raw.baseUrl.trim()) {
18165
+ out.baseUrl = raw.baseUrl.trim().replace(/\/+$/, "");
18166
+ }
18167
+ if ("apiKey" in raw) {
18168
+ problems.push(`${path4} contains "apiKey", which is ignored. Keys belong in ${"~/.devstation/credentials.json"}: run \`devstation login\`.`);
18169
+ }
18170
+ return out;
18171
+ } catch {
18172
+ problems.push(`${path4} is not valid JSON, so it was ignored.`);
18173
+ return {};
18174
+ }
18175
+ }
18176
+ function writeJson(path4, value, mode) {
18177
+ mkdirSync3(dirname4(path4), { recursive: true, mode: 448 });
18178
+ const partial = `${path4}.partial`;
18179
+ writeFileSync3(partial, `${JSON.stringify(value, null, 2)}
18180
+ `, { mode });
18181
+ chmodSync(partial, mode);
18182
+ renameSync(partial, path4);
18183
+ }
18184
+ function writeSettingsFile(path4, settings) {
18185
+ const clean = {};
18186
+ for (const key of SETTING_KEYS) {
18187
+ if (settings[key] !== undefined && settings[key] !== "") {
18188
+ clean[key] = settings[key];
18189
+ }
18190
+ }
18191
+ writeJson(path4, clean, 420);
18192
+ }
18193
+ function readCredentials(home, problems = []) {
18194
+ const path4 = credentialsPath(home);
18195
+ if (!existsSync4(path4))
18196
+ return {};
18197
+ try {
18198
+ const mode = statSync2(path4).mode & 511;
18199
+ if (mode & 63) {
18200
+ problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
18201
+ }
18202
+ const raw = JSON.parse(readFileSync3(path4, "utf8"));
18203
+ const out = {};
18204
+ for (const id of PROVIDER_IDS) {
18205
+ if (typeof raw[id] === "string" && raw[id].trim())
18206
+ out[id] = raw[id].trim();
18207
+ }
18208
+ return out;
18209
+ } catch {
18210
+ problems.push(`${path4} is not valid JSON, so no stored keys were read.`);
18211
+ return {};
18212
+ }
18213
+ }
18214
+ function writeCredentials(home, credentials) {
18215
+ mkdirSync3(globalDir(home), { recursive: true, mode: 448 });
18216
+ chmodSync(globalDir(home), 448);
18217
+ writeJson(credentialsPath(home), credentials, 384);
18218
+ }
18219
+ function resolveSettings(opts) {
18220
+ const env2 = opts.env ?? process.env;
18221
+ const home = opts.home ?? env2.HOME ?? "";
18222
+ const warnings = [];
18223
+ const project = readSettingsFile(projectConfigPath(opts.root), warnings);
18224
+ const global = home ? readSettingsFile(globalConfigPath(home), warnings) : {};
18225
+ const stored = home ? readCredentials(home, warnings) : {};
18226
+ const projectLabel = ".devstation/config.json";
18227
+ const globalLabel = "~/.devstation/config.json";
18228
+ let provider = null;
18229
+ let providerSource = "not set";
18230
+ if (env2.DEVSTATION_PROVIDER) {
18231
+ if (isProvider(env2.DEVSTATION_PROVIDER)) {
18232
+ provider = env2.DEVSTATION_PROVIDER;
18233
+ providerSource = "DEVSTATION_PROVIDER";
18234
+ } else {
18235
+ warnings.push(`DEVSTATION_PROVIDER="${env2.DEVSTATION_PROVIDER}" is not a provider, so it was ignored.`);
18236
+ }
18237
+ }
18238
+ if (!provider && project.provider) {
18239
+ provider = project.provider;
18240
+ providerSource = projectLabel;
18241
+ }
18242
+ if (!provider && global.provider) {
18243
+ provider = global.provider;
18244
+ providerSource = globalLabel;
18245
+ }
18246
+ if (!provider) {
18247
+ for (const id of ["anthropic", "openrouter", "openai"]) {
18248
+ const hit = KEY_ENV[id].find((name) => env2[name]);
18249
+ if (hit) {
18250
+ provider = id;
18251
+ providerSource = `inferred from ${hit}`;
18252
+ break;
18253
+ }
18254
+ }
18255
+ }
18256
+ if (!provider) {
18257
+ const only = PROVIDER_IDS.filter((id) => stored[id]);
18258
+ if (only.length >= 1) {
18259
+ provider = only[0];
18260
+ providerSource = "inferred from ~/.devstation/credentials.json";
18261
+ }
18262
+ }
18263
+ let model = null;
18264
+ let modelSource = "provider default";
18265
+ if (opts.model) {
18266
+ model = opts.model;
18267
+ modelSource = "--model";
18268
+ } else if (env2.DEVSTATION_MODEL) {
18269
+ model = env2.DEVSTATION_MODEL;
18270
+ modelSource = "DEVSTATION_MODEL";
18271
+ } else if (project.model) {
18272
+ model = project.model;
18273
+ modelSource = projectLabel;
18274
+ } else if (global.model) {
18275
+ model = global.model;
18276
+ modelSource = globalLabel;
18277
+ }
18278
+ let baseUrl = null;
18279
+ let baseUrlSource = "provider default";
18280
+ if (env2.DEVSTATION_BASE_URL) {
18281
+ baseUrl = env2.DEVSTATION_BASE_URL.replace(/\/+$/, "");
18282
+ baseUrlSource = "DEVSTATION_BASE_URL";
18283
+ } else if (project.baseUrl) {
18284
+ baseUrl = project.baseUrl;
18285
+ baseUrlSource = projectLabel;
18286
+ } else if (global.baseUrl) {
18287
+ baseUrl = global.baseUrl;
18288
+ baseUrlSource = globalLabel;
18289
+ }
18290
+ let apiKey = null;
18291
+ let keySource = "none";
18292
+ if (provider) {
18293
+ const hit = KEY_ENV[provider].find((name) => env2[name]);
18294
+ if (hit) {
18295
+ apiKey = env2[hit];
18296
+ keySource = hit;
18297
+ } else if (stored[provider]) {
18298
+ apiKey = stored[provider];
18299
+ keySource = "~/.devstation/credentials.json";
18300
+ }
18301
+ }
18302
+ let problem = null;
18303
+ if (!provider) {
18304
+ problem = "No model provider is configured. Run `devstation login`, or set ANTHROPIC_API_KEY or OPENROUTER_API_KEY.";
18305
+ } else if (!apiKey && provider !== "openai") {
18306
+ problem = `No API key for ${provider}. Run \`devstation login ${provider}\`, or set ${KEY_ENV[provider][0]}.`;
18307
+ } else if (provider === "openai" && !apiKey && !baseUrl) {
18308
+ problem = "No API key for openai. Run `devstation login openai`, set OPENAI_API_KEY, or point baseUrl at a local server that needs none.";
18309
+ } else if (provider === "openai" && !model) {
18310
+ problem = "The openai provider needs a model name, because every compatible server names them differently. Run `devstation config set model <name>`.";
18311
+ }
18312
+ return {
18313
+ provider,
18314
+ model,
18315
+ baseUrl,
18316
+ apiKey,
18317
+ source: {
18318
+ provider: providerSource,
18319
+ model: modelSource,
18320
+ baseUrl: baseUrlSource,
18321
+ apiKey: keySource
18322
+ },
18323
+ problem,
18324
+ warnings
18325
+ };
18326
+ }
18327
+ function maskKey(key) {
18328
+ if (!key)
18329
+ return "none";
18330
+ if (key.length <= 8)
18331
+ return "set";
18332
+ return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
18333
+ }
18334
+ // src/lib/agent/providers/index.ts
18335
+ function providerFromSettings(resolved) {
18336
+ if (resolved.problem || !resolved.provider)
18337
+ return null;
18338
+ const model = resolved.model ?? undefined;
18339
+ const baseUrl = resolved.baseUrl ?? undefined;
18340
+ switch (resolved.provider) {
18341
+ case "anthropic":
18342
+ return new AnthropicProvider({ apiKey: resolved.apiKey ?? undefined, model, baseUrl });
18343
+ case "openrouter":
18344
+ return new OpenRouterProvider(resolved.apiKey ?? "", model, { name: "openrouter", baseUrl });
18345
+ case "openai":
18346
+ return new OpenRouterProvider(resolved.apiKey ?? "", model, {
18347
+ name: "openai",
18348
+ baseUrl: baseUrl ?? "https://api.openai.com/v1"
18349
+ });
18350
+ }
18103
18351
  }
18104
18352
 
18105
18353
  // src/lib/agent/memory/embeddings.ts
@@ -18195,11 +18443,11 @@ async function embedMissing(store, provider, options = {}) {
18195
18443
  }
18196
18444
 
18197
18445
  // src/lib/agent/memory/workspace-index.ts
18198
- import { join as join6 } from "path";
18446
+ import { join as join7 } from "path";
18199
18447
 
18200
18448
  // src/lib/agent/repo-session.ts
18201
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
18202
- import { join as join5, relative as relative2, sep as sep2 } from "path";
18449
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
18450
+ import { join as join6, relative as relative2, sep as sep2 } from "path";
18203
18451
  var SKIP_DIRS = new Set([
18204
18452
  ".git",
18205
18453
  ".agent",
@@ -18233,7 +18481,7 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
18233
18481
  for (const item of readdirSync3(dir, { withFileTypes: true })) {
18234
18482
  if (item.isSymbolicLink())
18235
18483
  continue;
18236
- const full = join5(dir, item.name);
18484
+ const full = join6(dir, item.name);
18237
18485
  if (item.isDirectory()) {
18238
18486
  if (SKIP_DIRS.has(item.name))
18239
18487
  continue;
@@ -18242,9 +18490,9 @@ function readWorkspace(root, maxFileBytes = 1024 * 1024) {
18242
18490
  }
18243
18491
  if (!item.isFile())
18244
18492
  continue;
18245
- if (statSync2(full).size > maxFileBytes)
18493
+ if (statSync3(full).size > maxFileBytes)
18246
18494
  continue;
18247
- const buffer = readFileSync3(full);
18495
+ const buffer = readFileSync4(full);
18248
18496
  if (looksBinary(buffer))
18249
18497
  continue;
18250
18498
  files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
@@ -18281,8 +18529,8 @@ ${goal}` };
18281
18529
  // src/lib/agent/memory/store.ts
18282
18530
  import { Database } from "bun:sqlite";
18283
18531
  import { createHash } from "crypto";
18284
- import { mkdirSync as mkdirSync3 } from "fs";
18285
- import { dirname as dirname4 } from "path";
18532
+ import { mkdirSync as mkdirSync4 } from "fs";
18533
+ import { dirname as dirname5 } from "path";
18286
18534
 
18287
18535
  // src/lib/agent/memory/chunk.ts
18288
18536
  var BRACE_LANGUAGES = new Set([
@@ -18542,7 +18790,7 @@ class MemoryStore {
18542
18790
  constructor(path4) {
18543
18791
  this.path = path4;
18544
18792
  if (path4 !== ":memory:")
18545
- mkdirSync3(dirname4(path4), { recursive: true });
18793
+ mkdirSync4(dirname5(path4), { recursive: true });
18546
18794
  this.db = new Database(path4);
18547
18795
  this.db.run("PRAGMA journal_mode = WAL");
18548
18796
  this.migrate();
@@ -18715,7 +18963,7 @@ function toChunk(row) {
18715
18963
 
18716
18964
  // src/lib/agent/memory/workspace-index.ts
18717
18965
  function storePath(root) {
18718
- return join6(root, ".agent", "memory.db");
18966
+ return join7(root, ".agent", "memory.db");
18719
18967
  }
18720
18968
  function openStore(root) {
18721
18969
  return new MemoryStore(storePath(root));
@@ -18735,8 +18983,8 @@ async function indexWorkspace(root, options = {}) {
18735
18983
  }
18736
18984
 
18737
18985
  // src/lib/agent/memory/project-memory.ts
18738
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
18739
- import { dirname as dirname5, join as join7 } from "path";
18986
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
18987
+ import { dirname as dirname6, join as join8 } from "path";
18740
18988
  var DEFAULT_FILE = "PROJECT_MEMORY.md";
18741
18989
  var HEADER = `# Project memory
18742
18990
 
@@ -18745,16 +18993,16 @@ things about this project that are not in the code. Safe to edit or delete by
18745
18993
  hand: the agent only ever appends.
18746
18994
  `;
18747
18995
  function memoryPath(root) {
18748
- const atRoot = join7(root, DEFAULT_FILE);
18749
- if (existsSync4(atRoot))
18996
+ const atRoot = join8(root, DEFAULT_FILE);
18997
+ if (existsSync5(atRoot))
18750
18998
  return atRoot;
18751
- return join7(root, ".agent", DEFAULT_FILE);
18999
+ return join8(root, ".agent", DEFAULT_FILE);
18752
19000
  }
18753
19001
  function readMemory(root) {
18754
19002
  const path4 = memoryPath(root);
18755
- if (!existsSync4(path4))
19003
+ if (!existsSync5(path4))
18756
19004
  return [];
18757
- return parseMemory(readFileSync4(path4, "utf8"));
19005
+ return parseMemory(readFileSync5(path4, "utf8"));
18758
19006
  }
18759
19007
  var ENTRY = /^- \[([^\]]+)\](?:\s*\(([^)]*)\))?\s+([\s\S]*)$/;
18760
19008
  function parseMemory(text) {
@@ -18778,16 +19026,16 @@ function remember(root, note, tag = null, now = new Date) {
18778
19026
  if (!text) {
18779
19027
  return { ok: false, path: path4, message: "There was nothing to remember." };
18780
19028
  }
18781
- const existing = existsSync4(path4) ? readFileSync4(path4, "utf8") : "";
19029
+ const existing = existsSync5(path4) ? readFileSync5(path4, "utf8") : "";
18782
19030
  const entries = parseMemory(existing);
18783
19031
  const normal = (value) => value.toLowerCase().replace(/\s+/g, " ").trim();
18784
19032
  if (entries.some((entry) => normal(entry.note) === normal(text))) {
18785
19033
  return { ok: true, path: path4, duplicate: true, message: "Already remembered; nothing was added." };
18786
19034
  }
18787
19035
  const line = formatEntry({ note: text, tag, at: now.toISOString() });
18788
- mkdirSync4(dirname5(path4), { recursive: true });
19036
+ mkdirSync5(dirname6(path4), { recursive: true });
18789
19037
  const body = existing || HEADER;
18790
- writeFileSync3(path4, `${body.replace(/\n+$/, "")}
19038
+ writeFileSync4(path4, `${body.replace(/\n+$/, "")}
18791
19039
  ${line}
18792
19040
  `);
18793
19041
  return { ok: true, path: path4, message: `Remembered, in ${DEFAULT_FILE}.` };
@@ -18814,22 +19062,22 @@ function renderMemory(entries) {
18814
19062
 
18815
19063
  // src/lib/agent/mcp.ts
18816
19064
  import { spawn as spawn3 } from "child_process";
18817
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
18818
- import { join as join8 } from "path";
19065
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
19066
+ import { join as join9 } from "path";
18819
19067
  function configPaths(root, home = process.env.HOME ?? "") {
18820
19068
  return [
18821
- join8(root, ".devstation", "mcp.json"),
18822
- join8(root, ".mcp.json"),
18823
- ...home ? [join8(home, ".devstation", "mcp.json")] : []
19069
+ join9(root, ".devstation", "mcp.json"),
19070
+ join9(root, ".mcp.json"),
19071
+ ...home ? [join9(home, ".devstation", "mcp.json")] : []
18824
19072
  ];
18825
19073
  }
18826
19074
  function loadConfig(root, home = process.env.HOME ?? "") {
18827
19075
  const merged = { mcpServers: {} };
18828
19076
  for (const path4 of configPaths(root, home).reverse()) {
18829
- if (!existsSync5(path4))
19077
+ if (!existsSync6(path4))
18830
19078
  continue;
18831
19079
  try {
18832
- const parsed = JSON.parse(readFileSync5(path4, "utf8"));
19080
+ const parsed = JSON.parse(readFileSync6(path4, "utf8"));
18833
19081
  for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
18834
19082
  if (server && typeof server.command === "string")
18835
19083
  merged.mcpServers[name] = server;
@@ -19089,12 +19337,12 @@ function failedResult(message) {
19089
19337
 
19090
19338
  // src/lib/agent/sandbox-exec.ts
19091
19339
  import { randomBytes } from "crypto";
19092
- import { existsSync as existsSync7, statSync as statSync4, unlinkSync } from "fs";
19093
- import { isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
19340
+ import { existsSync as existsSync8, statSync as statSync5, unlinkSync } from "fs";
19341
+ import { isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
19094
19342
 
19095
19343
  // src/lib/agent/project.ts
19096
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
19097
- import { dirname as dirname6, join as join9, relative as relative3, sep as sep3 } from "path";
19344
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
19345
+ import { dirname as dirname7, join as join10, relative as relative3, sep as sep3 } from "path";
19098
19346
  var SKIP = new Set([
19099
19347
  "node_modules",
19100
19348
  ".git",
@@ -19120,7 +19368,7 @@ var MANIFESTS = [
19120
19368
  var MAX_DEPTH = 2;
19121
19369
  function readScripts(absolute) {
19122
19370
  try {
19123
- const parsed = JSON.parse(readFileSync6(absolute, "utf8"));
19371
+ const parsed = JSON.parse(readFileSync7(absolute, "utf8"));
19124
19372
  return parsed.scripts ?? {};
19125
19373
  } catch {
19126
19374
  return {};
@@ -19130,8 +19378,8 @@ function detectManifests(root) {
19130
19378
  const found = [];
19131
19379
  const scan = (dir, depth) => {
19132
19380
  for (const { file, ecosystem } of MANIFESTS) {
19133
- const absolute = join9(dir, file);
19134
- if (!existsSync6(absolute))
19381
+ const absolute = join10(dir, file);
19382
+ if (!existsSync7(absolute))
19135
19383
  continue;
19136
19384
  found.push({
19137
19385
  dir: relative3(root, dir).split(sep3).join("/"),
@@ -19151,9 +19399,9 @@ function detectManifests(root) {
19151
19399
  for (const entry of entries) {
19152
19400
  if (SKIP.has(entry) || entry.startsWith("."))
19153
19401
  continue;
19154
- const child = join9(dir, entry);
19402
+ const child = join10(dir, entry);
19155
19403
  try {
19156
- if (statSync3(child).isDirectory())
19404
+ if (statSync4(child).isDirectory())
19157
19405
  scan(child, depth + 1);
19158
19406
  } catch {}
19159
19407
  }
@@ -19240,7 +19488,7 @@ function lockfilesFor(manifest) {
19240
19488
  }
19241
19489
  }
19242
19490
  function cwdFor(root, manifest) {
19243
- return manifest.dir ? join9(root, manifest.dir) : root;
19491
+ return manifest.dir ? join10(root, manifest.dir) : root;
19244
19492
  }
19245
19493
 
19246
19494
  // src/lib/agent/sandbox-exec.ts
@@ -19415,16 +19663,16 @@ function sandboxExecutor(options) {
19415
19663
  };
19416
19664
  const verifyOwnership = async (name) => {
19417
19665
  const probe = `.devstation-uid-probe-${session}`;
19418
- const path4 = join10(options.workspace, probe);
19666
+ const path4 = join11(options.workspace, probe);
19419
19667
  const written = await runShell(execCommand(name, `touch ${quote2(probe)}`, 20), {
19420
19668
  cwd: options.workspace,
19421
19669
  timeoutMs: 30000
19422
19670
  });
19423
- if (!written.ok || !existsSync7(path4)) {
19671
+ if (!written.ok || !existsSync8(path4)) {
19424
19672
  return "The sandbox could not write to the workspace. Check the mount and try --no-sandbox.";
19425
19673
  }
19426
19674
  try {
19427
- const stat2 = statSync4(path4);
19675
+ const stat2 = statSync5(path4);
19428
19676
  const [uid, gid] = user.split(":").map(Number);
19429
19677
  if (stat2.uid !== uid || stat2.gid !== gid) {
19430
19678
  return `The sandbox writes files as ${stat2.uid}:${stat2.gid} but this account is ${user}. ` + "Files it creates would not be yours to edit, and git would stop trusting the " + "repository, so it was not started. Run with --no-sandbox, or set " + "DEVSTATION_SANDBOX_USER.";
@@ -20475,37 +20723,37 @@ function summarise(call, outcome) {
20475
20723
  // src/lib/agent/session-store.ts
20476
20724
  import {
20477
20725
  appendFileSync,
20478
- existsSync as existsSync8,
20479
- mkdirSync as mkdirSync5,
20480
- readFileSync as readFileSync7,
20726
+ existsSync as existsSync9,
20727
+ mkdirSync as mkdirSync6,
20728
+ readFileSync as readFileSync8,
20481
20729
  readdirSync as readdirSync6,
20482
- renameSync,
20483
- statSync as statSync5,
20484
- writeFileSync as writeFileSync4
20730
+ renameSync as renameSync2,
20731
+ statSync as statSync6,
20732
+ writeFileSync as writeFileSync5
20485
20733
  } from "fs";
20486
- import { join as join11 } from "path";
20734
+ import { join as join12 } from "path";
20487
20735
  import { randomUUID as randomUUID2 } from "crypto";
20488
- var SESSIONS_DIR = join11(".agent", "sessions");
20736
+ var SESSIONS_DIR = join12(".agent", "sessions");
20489
20737
 
20490
20738
  class SessionStore {
20491
20739
  root;
20492
20740
  dir;
20493
20741
  constructor(root) {
20494
20742
  this.root = root;
20495
- this.dir = join11(root, SESSIONS_DIR);
20743
+ this.dir = join12(root, SESSIONS_DIR);
20496
20744
  }
20497
20745
  ensure() {
20498
- mkdirSync5(this.dir, { recursive: true });
20499
- const ignore = join11(this.dir, "..", ".gitignore");
20500
- if (!existsSync8(ignore))
20501
- writeFileSync4(ignore, `*
20746
+ mkdirSync6(this.dir, { recursive: true });
20747
+ const ignore = join12(this.dir, "..", ".gitignore");
20748
+ if (!existsSync9(ignore))
20749
+ writeFileSync5(ignore, `*
20502
20750
  `);
20503
20751
  }
20504
20752
  jsonPath(id) {
20505
- return join11(this.dir, `${id}.json`);
20753
+ return join12(this.dir, `${id}.json`);
20506
20754
  }
20507
20755
  eventPath(id) {
20508
- return join11(this.dir, `${id}.jsonl`);
20756
+ return join12(this.dir, `${id}.jsonl`);
20509
20757
  }
20510
20758
  create(goal, meta) {
20511
20759
  this.ensure();
@@ -20535,21 +20783,21 @@ class SessionStore {
20535
20783
  record.updatedAt = new Date().toISOString();
20536
20784
  const target = this.jsonPath(record.id);
20537
20785
  const temporary = `${target}.tmp`;
20538
- writeFileSync4(temporary, JSON.stringify(record, null, 2));
20539
- renameSync(temporary, target);
20786
+ writeFileSync5(temporary, JSON.stringify(record, null, 2));
20787
+ renameSync2(temporary, target);
20540
20788
  }
20541
20789
  load(id) {
20542
20790
  const path4 = this.jsonPath(id);
20543
- if (!existsSync8(path4))
20791
+ if (!existsSync9(path4))
20544
20792
  return null;
20545
20793
  try {
20546
- return JSON.parse(readFileSync7(path4, "utf8"));
20794
+ return JSON.parse(readFileSync8(path4, "utf8"));
20547
20795
  } catch {
20548
20796
  return null;
20549
20797
  }
20550
20798
  }
20551
20799
  list() {
20552
- if (!existsSync8(this.dir))
20800
+ if (!existsSync9(this.dir))
20553
20801
  return [];
20554
20802
  return readdirSync6(this.dir).filter((name) => name.endsWith(".json")).map((name) => this.load(name.slice(0, -".json".length))).filter((record) => record !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
20555
20803
  }
@@ -20563,12 +20811,12 @@ class SessionStore {
20563
20811
  }
20564
20812
  readEvents(id, fromByte = 0) {
20565
20813
  const path4 = this.eventPath(id);
20566
- if (!existsSync8(path4))
20814
+ if (!existsSync9(path4))
20567
20815
  return { events: [], offset: 0 };
20568
- const size = statSync5(path4).size;
20816
+ const size = statSync6(path4).size;
20569
20817
  if (size <= fromByte)
20570
20818
  return { events: [], offset: size };
20571
- const text = readFileSync7(path4, "utf8").slice(fromByte);
20819
+ const text = readFileSync8(path4, "utf8").slice(fromByte);
20572
20820
  const events = [];
20573
20821
  let consumed = 0;
20574
20822
  for (const line of text.split(`
@@ -20962,7 +21210,17 @@ function configCommand(context) {
20962
21210
  context.terminal.out(JSON.stringify({
20963
21211
  workspace: context.root,
20964
21212
  git: isRepo(context.root),
20965
- provider: context.provider ? { name: context.provider.name, model: context.provider.model } : configuredProviderName() ?? null,
21213
+ provider: context.provider ? { name: context.provider.name, model: context.provider.model } : (() => {
21214
+ const r2 = resolveSettings({ root: context.root });
21215
+ return {
21216
+ name: r2.provider,
21217
+ model: r2.model,
21218
+ baseUrl: r2.baseUrl,
21219
+ key: maskKey(r2.apiKey),
21220
+ source: r2.source,
21221
+ problem: r2.problem
21222
+ };
21223
+ })(),
20966
21224
  autonomy: context.autonomy ?? "ask_sensitive",
20967
21225
  maxSteps: context.maxSteps ?? 40,
20968
21226
  budgetUsd: context.maxCostUsd ?? null,
@@ -20971,10 +21229,14 @@ function configCommand(context) {
20971
21229
  }, null, 2));
20972
21230
  return 0;
20973
21231
  }
21232
+ const r = resolveSettings({ root: context.root });
20974
21233
  const lines = [
20975
21234
  `workspace ${context.root}`,
20976
21235
  `git ${isRepo(context.root) ? "yes" : "no, so checkpoints are file snapshots under .devstation/"}`,
20977
- `provider ${context.provider ? `${context.provider.name}/${context.provider.model}` : configuredProviderName() ?? "none configured, set ANTHROPIC_API_KEY or OPENROUTER_API_KEY"}`,
21236
+ context.provider ? `provider ${context.provider.name}/${context.provider.model} (this session)` : `provider ${r.provider ?? "none"} (${r.source.provider})`,
21237
+ `model ${context.provider ? context.provider.model : r.model ?? "provider default"} (${context.provider ? "this session" : r.source.model})`,
21238
+ `endpoint ${r.baseUrl ?? (r.provider ? DEFAULT_BASE_URL[r.provider] : "none")} (${r.source.baseUrl})`,
21239
+ `api key ${maskKey(r.apiKey)} (${r.source.apiKey})`,
20978
21240
  `autonomy ${context.autonomy ?? "ask_sensitive"}`,
20979
21241
  `max steps ${context.maxSteps ?? 40}`,
20980
21242
  `budget ${context.maxCostUsd ? `$${context.maxCostUsd}` : "none set"}`,
@@ -20982,16 +21244,163 @@ function configCommand(context) {
20982
21244
  ];
20983
21245
  for (const line of lines)
20984
21246
  context.terminal.out(line);
21247
+ if (r.problem && !context.provider)
21248
+ context.terminal.err(`
21249
+ ${r.problem}`);
21250
+ for (const warning of r.warnings)
21251
+ context.terminal.err(`warning: ${warning}`);
21252
+ return 0;
21253
+ }
21254
+ function home() {
21255
+ return process.env.HOME ?? "";
21256
+ }
21257
+ function configEditCommand(context, rest, opts = {}) {
21258
+ const [action, key, ...valueWords] = rest.trim().split(/\s+/);
21259
+ const value = valueWords.join(" ").trim();
21260
+ const path4 = opts.project ? projectConfigPath(context.root) : globalConfigPath(home());
21261
+ if (action === "path") {
21262
+ context.terminal.out(`global ${globalConfigPath(home())}`);
21263
+ context.terminal.out(`project ${projectConfigPath(context.root)}`);
21264
+ context.terminal.out(`credentials ${credentialsPath(home())}`);
21265
+ return 0;
21266
+ }
21267
+ if (key === "apiKey" || key === "key") {
21268
+ context.terminal.err(`API keys are not stored in config. Run \`devstation login\` to store one in ${credentialsPath(home())}.`);
21269
+ return 2;
21270
+ }
21271
+ if (!key || !SETTING_KEYS.includes(key)) {
21272
+ context.terminal.err(`Unknown setting "${key ?? ""}". Settings: ${SETTING_KEYS.join(", ")}.`);
21273
+ return 2;
21274
+ }
21275
+ const name = key;
21276
+ const current = readSettingsFile(path4);
21277
+ if (action === "get") {
21278
+ context.terminal.out(current[name] ?? "");
21279
+ return 0;
21280
+ }
21281
+ if (action === "unset") {
21282
+ delete current[name];
21283
+ writeSettingsFile(path4, current);
21284
+ context.terminal.out(`Removed ${name} from ${path4}.`);
21285
+ return 0;
21286
+ }
21287
+ if (action === "set") {
21288
+ if (!value) {
21289
+ context.terminal.err(`Give it a value: devstation config set ${name} <value>`);
21290
+ return 2;
21291
+ }
21292
+ if (name === "provider" && !PROVIDER_IDS.includes(value)) {
21293
+ context.terminal.err(`Unknown provider "${value}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21294
+ return 2;
21295
+ }
21296
+ if (name === "baseUrl" && !/^https?:\/\//.test(value)) {
21297
+ context.terminal.err("baseUrl must start with http:// or https://.");
21298
+ return 2;
21299
+ }
21300
+ current[name] = name === "baseUrl" ? value.replace(/\/+$/, "") : value;
21301
+ writeSettingsFile(path4, current);
21302
+ context.terminal.out(`Set ${name} = ${current[name]} in ${path4}.`);
21303
+ return 0;
21304
+ }
21305
+ context.terminal.err("Use: devstation config set <key> <value> | get <key> | unset <key> | path (add --project for this workspace)");
21306
+ return 2;
21307
+ }
21308
+ async function loginCommand(context, rest) {
21309
+ const t = context.terminal;
21310
+ const secret = t.askSecret ? (q) => t.askSecret(q) : (q) => t.ask(q);
21311
+ let provider = rest.trim().split(/\s+/)[0];
21312
+ if (!provider) {
21313
+ t.out("Which provider?");
21314
+ t.out(" anthropic Claude, directly (prompt caching, native tool use)");
21315
+ t.out(" openrouter one key for Claude, GPT, Gemini, DeepSeek and more");
21316
+ t.out(" openai OpenAI, or any compatible server: Ollama, LM Studio, Groq, Together");
21317
+ provider = (await t.ask("provider [anthropic]: ")).trim() || "anthropic";
21318
+ }
21319
+ if (!PROVIDER_IDS.includes(provider)) {
21320
+ t.err(`Unknown provider "${provider}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21321
+ return 2;
21322
+ }
21323
+ const id = provider;
21324
+ let baseUrl = "";
21325
+ if (id === "openai") {
21326
+ baseUrl = (await t.ask(`endpoint [${DEFAULT_BASE_URL.openai}]: `)).trim();
21327
+ if (baseUrl && !/^https?:\/\//.test(baseUrl)) {
21328
+ t.err("The endpoint must start with http:// or https://.");
21329
+ return 2;
21330
+ }
21331
+ }
21332
+ const key = (await secret(id === "openai" ? "API key (blank for a local server): " : "API key: ")).trim();
21333
+ if (!key && id !== "openai") {
21334
+ t.err("No key entered, so nothing was saved.");
21335
+ return 2;
21336
+ }
21337
+ const modelHint = id === "anthropic" ? "claude-sonnet-5" : id === "openrouter" ? "anthropic/claude-sonnet-5" : "required";
21338
+ const model = (await t.ask(`model [${modelHint}]: `)).trim();
21339
+ if (id === "openai" && !model) {
21340
+ t.err("The openai provider needs a model name, because every compatible server names them differently.");
21341
+ return 2;
21342
+ }
21343
+ const h = home();
21344
+ if (!h) {
21345
+ t.err("HOME is not set, so there is nowhere to store settings.");
21346
+ return 2;
21347
+ }
21348
+ if (key) {
21349
+ const credentials = readCredentials(h);
21350
+ credentials[id] = key;
21351
+ writeCredentials(h, credentials);
21352
+ }
21353
+ const settings2 = readSettingsFile(globalConfigPath(h));
21354
+ settings2.provider = id;
21355
+ if (model)
21356
+ settings2.model = model;
21357
+ else
21358
+ delete settings2.model;
21359
+ if (baseUrl)
21360
+ settings2.baseUrl = baseUrl.replace(/\/+$/, "");
21361
+ else if (id !== "openai")
21362
+ delete settings2.baseUrl;
21363
+ writeSettingsFile(globalConfigPath(h), settings2);
21364
+ t.out("");
21365
+ t.out(`Saved. provider ${id}, model ${model || "provider default"}${baseUrl ? `, endpoint ${baseUrl}` : ""}`);
21366
+ if (key)
21367
+ t.out(`key ${maskKey(key)} in ${credentialsPath(h)} (readable only by you)`);
21368
+ t.out(`settings in ${globalConfigPath(h)}`);
21369
+ t.out("");
21370
+ t.out("Check it with: devstation doctor");
21371
+ return 0;
21372
+ }
21373
+ function logoutCommand(context, rest) {
21374
+ const h = home();
21375
+ const target = rest.trim().split(/\s+/)[0];
21376
+ if (target && !PROVIDER_IDS.includes(target)) {
21377
+ context.terminal.err(`Unknown provider "${target}". Providers: ${PROVIDER_IDS.join(", ")}.`);
21378
+ return 2;
21379
+ }
21380
+ const credentials = readCredentials(h);
21381
+ const removed = target ? credentials[target] ? [target] : [] : Object.keys(credentials);
21382
+ if (removed.length === 0) {
21383
+ context.terminal.out(target ? `No stored key for ${target}.` : "No stored keys.");
21384
+ return 0;
21385
+ }
21386
+ if (target)
21387
+ delete credentials[target];
21388
+ writeCredentials(h, target ? credentials : {});
21389
+ context.terminal.out(`Removed the stored key for ${removed.join(", ")}.`);
21390
+ context.terminal.out("Keys exported in your shell (ANTHROPIC_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY) are not touched.");
20985
21391
  return 0;
20986
21392
  }
20987
21393
  async function runChecks(root, env2 = process.env) {
20988
21394
  const checks = [];
20989
- const provider = configuredProviderName(env2);
21395
+ const settings2 = resolveSettings({ root, env: env2 });
20990
21396
  checks.push({
20991
21397
  name: "model provider",
20992
- ok: provider !== null,
20993
- detail: provider ?? "set ANTHROPIC_API_KEY, or OPENROUTER_API_KEY"
21398
+ ok: settings2.problem === null,
21399
+ detail: settings2.problem ?? `${settings2.provider}${settings2.model ? `/${settings2.model}` : ""}, key from ${settings2.source.apiKey}`
20994
21400
  });
21401
+ for (const warning of settings2.warnings) {
21402
+ checks.push({ name: "settings", ok: false, detail: warning });
21403
+ }
20995
21404
  const git2 = await runShell("git --version", { cwd: root, timeoutMs: 15000 });
20996
21405
  checks.push({
20997
21406
  name: "git",
@@ -21000,7 +21409,7 @@ async function runChecks(root, env2 = process.env) {
21000
21409
  });
21001
21410
  checks.push({
21002
21411
  name: "workspace",
21003
- ok: existsSync9(root),
21412
+ ok: existsSync10(root),
21004
21413
  detail: isRepo(root) ? `${root} (a git repository)` : `${root} (not a git repository)`
21005
21414
  });
21006
21415
  let writable = false;
@@ -21133,11 +21542,11 @@ function wordmark(ink = "\u2588") {
21133
21542
  return rows;
21134
21543
  }
21135
21544
  var WORDMARK_WIDTH = wordmark()[0].length;
21136
- function shortPath(path4, home = process.env.HOME ?? "") {
21137
- if (home && path4 === home)
21545
+ function shortPath(path4, home2 = process.env.HOME ?? "") {
21546
+ if (home2 && path4 === home2)
21138
21547
  return "~";
21139
- if (home && path4.startsWith(`${home}/`))
21140
- return `~${path4.slice(home.length)}`;
21548
+ if (home2 && path4.startsWith(`${home2}/`))
21549
+ return `~${path4.slice(home2.length)}`;
21141
21550
  return path4;
21142
21551
  }
21143
21552
  function banner(facts, options = {}) {
@@ -21301,8 +21710,8 @@ async function chatCommand(context, opening = "") {
21301
21710
  }
21302
21711
 
21303
21712
  // src/lib/agent/cli/repo-command.ts
21304
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
21305
- import { join as join12 } from "path";
21713
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, existsSync as existsSync11 } from "fs";
21714
+ import { join as join13 } from "path";
21306
21715
 
21307
21716
  // src/lib/github-repos.ts
21308
21717
  var API = "https://api.github.com";
@@ -21546,11 +21955,11 @@ function filesFromArchive(entries, limits = {}) {
21546
21955
  // src/lib/agent/cli/repo-command.ts
21547
21956
  function workspaceFor(root, owner, name, now) {
21548
21957
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..*/, "").replace("T", "-");
21549
- const dir = join12(root, ".devstation", "repos", `${owner}-${name}-${stamp}`);
21550
- mkdirSync6(dir, { recursive: true });
21551
- const ignore = join12(root, ".devstation", ".gitignore");
21552
- if (!existsSync10(ignore))
21553
- writeFileSync5(ignore, `*
21958
+ const dir = join13(root, ".devstation", "repos", `${owner}-${name}-${stamp}`);
21959
+ mkdirSync7(dir, { recursive: true });
21960
+ const ignore = join13(root, ".devstation", ".gitignore");
21961
+ if (!existsSync11(ignore))
21962
+ writeFileSync6(ignore, `*
21554
21963
  `);
21555
21964
  return dir;
21556
21965
  }
@@ -21745,12 +22154,24 @@ ${HELP}`);
21745
22154
  return 0;
21746
22155
  }
21747
22156
  const root = resolve4(parsed.root);
21748
- if (!existsSync11(root)) {
22157
+ if (!existsSync12(root)) {
21749
22158
  process.stderr.write(`There is no directory at ${root}.
21750
22159
  `);
21751
22160
  return 2;
21752
22161
  }
21753
- const readline2 = createInterface2({ input: process.stdin, output: process.stdout });
22162
+ let muted = false;
22163
+ const output = new Writable({
22164
+ write(chunk, encoding, done) {
22165
+ if (!muted)
22166
+ process.stdout.write(chunk, encoding);
22167
+ done();
22168
+ }
22169
+ });
22170
+ const readline2 = createInterface2({
22171
+ input: process.stdin,
22172
+ output,
22173
+ terminal: Boolean(process.stdin.isTTY)
22174
+ });
21754
22175
  const input = lineReader(readline2, (text) => process.stdout.write(text));
21755
22176
  const terminal = {
21756
22177
  out: (text) => process.stdout.write(`${text}
@@ -21758,6 +22179,18 @@ ${HELP}`);
21758
22179
  err: (text) => process.stderr.write(`${text}
21759
22180
  `),
21760
22181
  ask: (question) => input.ask(question),
22182
+ askSecret: async (question) => {
22183
+ process.stdout.write(question);
22184
+ muted = true;
22185
+ try {
22186
+ return await input.ask("");
22187
+ } finally {
22188
+ muted = false;
22189
+ if (process.stdin.isTTY)
22190
+ process.stdout.write(`
22191
+ `);
22192
+ }
22193
+ },
21761
22194
  write: process.stdout.isTTY ? (text) => process.stdout.write(text) : undefined,
21762
22195
  colour: colourEnabled()
21763
22196
  };
@@ -21784,7 +22217,11 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
21784
22217
  case "tools":
21785
22218
  return toolsCommand(offline);
21786
22219
  case "config":
21787
- return configCommand(offline);
22220
+ return parsed.rest.trim() ? configEditCommand(offline, parsed.rest, { project: parsed.project }) : configCommand(offline);
22221
+ case "login":
22222
+ return await loginCommand(offline, parsed.rest);
22223
+ case "logout":
22224
+ return logoutCommand(offline, parsed.rest);
21788
22225
  case "doctor":
21789
22226
  return await doctorCommand(offline);
21790
22227
  case "index":
@@ -21796,9 +22233,12 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
21796
22233
  default:
21797
22234
  break;
21798
22235
  }
21799
- const provider = providerFromEnv(process.env, parsed.model);
22236
+ const settings2 = resolveSettings({ root, model: parsed.model });
22237
+ for (const warning of settings2.warnings)
22238
+ terminal.err(`warning: ${warning}`);
22239
+ const provider = providerFromSettings(settings2);
21800
22240
  if (!provider) {
21801
- terminal.err("No model provider is configured. Set ANTHROPIC_API_KEY (or OPENROUTER_API_KEY) and try again.");
22241
+ terminal.err(settings2.problem ?? "No model provider is configured.");
21802
22242
  terminal.err(`Run \`${CLI_NAME} doctor\` to see what else is missing.`);
21803
22243
  return 2;
21804
22244
  }
@@ -21824,7 +22264,7 @@ Stopping. The session is saved; resume it with \`${CLI_NAME} resume\`.`);
21824
22264
  terminal.err(`Give it something to do: ${CLI_NAME} run "fix the failing test in src/lib"`);
21825
22265
  return 2;
21826
22266
  }
21827
- terminal.out(`using ${configuredProviderName()}`);
22267
+ terminal.out(`using ${provider.name}/${provider.model}`);
21828
22268
  const { code } = await runCommand(ctx, parsed.rest);
21829
22269
  return code;
21830
22270
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstationlabs/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The DevStation coding agent, in your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",