@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.
- package/README.md +13 -5
- package/devstation.js +556 -116
- 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
|
-
##
|
|
30
|
+
## Set up a model
|
|
31
31
|
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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}
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
18002
|
+
name;
|
|
17984
18003
|
model;
|
|
17985
|
-
|
|
18004
|
+
endpoint;
|
|
18005
|
+
constructor(apiKey, model, opts = {}) {
|
|
17986
18006
|
this.apiKey = apiKey;
|
|
17987
|
-
this.
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
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
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
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
|
|
18446
|
+
import { join as join7 } from "path";
|
|
18199
18447
|
|
|
18200
18448
|
// src/lib/agent/repo-session.ts
|
|
18201
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
18202
|
-
import { join as
|
|
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 =
|
|
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 (
|
|
18493
|
+
if (statSync3(full).size > maxFileBytes)
|
|
18246
18494
|
continue;
|
|
18247
|
-
const buffer =
|
|
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
|
|
18285
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
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
|
|
18739
|
-
import { dirname as
|
|
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 =
|
|
18749
|
-
if (
|
|
18996
|
+
const atRoot = join8(root, DEFAULT_FILE);
|
|
18997
|
+
if (existsSync5(atRoot))
|
|
18750
18998
|
return atRoot;
|
|
18751
|
-
return
|
|
18999
|
+
return join8(root, ".agent", DEFAULT_FILE);
|
|
18752
19000
|
}
|
|
18753
19001
|
function readMemory(root) {
|
|
18754
19002
|
const path4 = memoryPath(root);
|
|
18755
|
-
if (!
|
|
19003
|
+
if (!existsSync5(path4))
|
|
18756
19004
|
return [];
|
|
18757
|
-
return parseMemory(
|
|
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 =
|
|
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
|
-
|
|
19036
|
+
mkdirSync5(dirname6(path4), { recursive: true });
|
|
18789
19037
|
const body = existing || HEADER;
|
|
18790
|
-
|
|
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
|
|
18818
|
-
import { join as
|
|
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
|
-
|
|
18822
|
-
|
|
18823
|
-
...home ? [
|
|
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 (!
|
|
19077
|
+
if (!existsSync6(path4))
|
|
18830
19078
|
continue;
|
|
18831
19079
|
try {
|
|
18832
|
-
const parsed = JSON.parse(
|
|
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
|
|
19093
|
-
import { isAbsolute as isAbsolute3, join as
|
|
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
|
|
19097
|
-
import { dirname as
|
|
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(
|
|
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 =
|
|
19134
|
-
if (!
|
|
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 =
|
|
19402
|
+
const child = join10(dir, entry);
|
|
19155
19403
|
try {
|
|
19156
|
-
if (
|
|
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 ?
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
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
|
|
20479
|
-
mkdirSync as
|
|
20480
|
-
readFileSync as
|
|
20726
|
+
existsSync as existsSync9,
|
|
20727
|
+
mkdirSync as mkdirSync6,
|
|
20728
|
+
readFileSync as readFileSync8,
|
|
20481
20729
|
readdirSync as readdirSync6,
|
|
20482
|
-
renameSync,
|
|
20483
|
-
statSync as
|
|
20484
|
-
writeFileSync as
|
|
20730
|
+
renameSync as renameSync2,
|
|
20731
|
+
statSync as statSync6,
|
|
20732
|
+
writeFileSync as writeFileSync5
|
|
20485
20733
|
} from "fs";
|
|
20486
|
-
import { join as
|
|
20734
|
+
import { join as join12 } from "path";
|
|
20487
20735
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
20488
|
-
var SESSIONS_DIR =
|
|
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 =
|
|
20743
|
+
this.dir = join12(root, SESSIONS_DIR);
|
|
20496
20744
|
}
|
|
20497
20745
|
ensure() {
|
|
20498
|
-
|
|
20499
|
-
const ignore =
|
|
20500
|
-
if (!
|
|
20501
|
-
|
|
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
|
|
20753
|
+
return join12(this.dir, `${id}.json`);
|
|
20506
20754
|
}
|
|
20507
20755
|
eventPath(id) {
|
|
20508
|
-
return
|
|
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
|
-
|
|
20539
|
-
|
|
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 (!
|
|
20791
|
+
if (!existsSync9(path4))
|
|
20544
20792
|
return null;
|
|
20545
20793
|
try {
|
|
20546
|
-
return JSON.parse(
|
|
20794
|
+
return JSON.parse(readFileSync8(path4, "utf8"));
|
|
20547
20795
|
} catch {
|
|
20548
20796
|
return null;
|
|
20549
20797
|
}
|
|
20550
20798
|
}
|
|
20551
20799
|
list() {
|
|
20552
|
-
if (!
|
|
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 (!
|
|
20814
|
+
if (!existsSync9(path4))
|
|
20567
20815
|
return { events: [], offset: 0 };
|
|
20568
|
-
const size =
|
|
20816
|
+
const size = statSync6(path4).size;
|
|
20569
20817
|
if (size <= fromByte)
|
|
20570
20818
|
return { events: [], offset: size };
|
|
20571
|
-
const text =
|
|
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 } :
|
|
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
|
-
|
|
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
|
|
21395
|
+
const settings2 = resolveSettings({ root, env: env2 });
|
|
20990
21396
|
checks.push({
|
|
20991
21397
|
name: "model provider",
|
|
20992
|
-
ok:
|
|
20993
|
-
detail:
|
|
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:
|
|
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,
|
|
21137
|
-
if (
|
|
21545
|
+
function shortPath(path4, home2 = process.env.HOME ?? "") {
|
|
21546
|
+
if (home2 && path4 === home2)
|
|
21138
21547
|
return "~";
|
|
21139
|
-
if (
|
|
21140
|
-
return `~${path4.slice(
|
|
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
|
|
21305
|
-
import { join as
|
|
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 =
|
|
21550
|
-
|
|
21551
|
-
const ignore =
|
|
21552
|
-
if (!
|
|
21553
|
-
|
|
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 (!
|
|
22157
|
+
if (!existsSync12(root)) {
|
|
21749
22158
|
process.stderr.write(`There is no directory at ${root}.
|
|
21750
22159
|
`);
|
|
21751
22160
|
return 2;
|
|
21752
22161
|
}
|
|
21753
|
-
|
|
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
|
|
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.
|
|
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 ${
|
|
22267
|
+
terminal.out(`using ${provider.name}/${provider.model}`);
|
|
21828
22268
|
const { code } = await runCommand(ctx, parsed.rest);
|
|
21829
22269
|
return code;
|
|
21830
22270
|
} finally {
|