@devstationlabs/cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devstation.js +303 -190
- package/package.json +1 -1
package/devstation.js
CHANGED
|
@@ -12510,7 +12510,7 @@ import { existsSync as existsSync12 } from "fs";
|
|
|
12510
12510
|
|
|
12511
12511
|
// src/lib/agent/cli/args.ts
|
|
12512
12512
|
var CLI_NAME = "devstation";
|
|
12513
|
-
var VERSION = "0.1.
|
|
12513
|
+
var VERSION = "0.1.5";
|
|
12514
12514
|
var COMMANDS = new Set([
|
|
12515
12515
|
"chat",
|
|
12516
12516
|
"run",
|
|
@@ -13083,12 +13083,16 @@ import {
|
|
|
13083
13083
|
cpSync,
|
|
13084
13084
|
existsSync as existsSync3,
|
|
13085
13085
|
mkdirSync as mkdirSync2,
|
|
13086
|
-
readFileSync as
|
|
13087
|
-
readdirSync as
|
|
13086
|
+
readFileSync as readFileSync4,
|
|
13087
|
+
readdirSync as readdirSync3,
|
|
13088
13088
|
rmSync,
|
|
13089
13089
|
writeFileSync as writeFileSync2
|
|
13090
13090
|
} from "fs";
|
|
13091
|
-
import { dirname as dirname2, join as
|
|
13091
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
13092
|
+
|
|
13093
|
+
// src/lib/agent/repo-session.ts
|
|
13094
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
13095
|
+
import { join as join3, relative as relative2, sep as sep2 } from "path";
|
|
13092
13096
|
|
|
13093
13097
|
// src/lib/agent/workspace.ts
|
|
13094
13098
|
import {
|
|
@@ -13239,23 +13243,219 @@ class Workspace {
|
|
|
13239
13243
|
}
|
|
13240
13244
|
}
|
|
13241
13245
|
|
|
13246
|
+
// src/lib/agent/repo-session.ts
|
|
13247
|
+
var SKIP_DIRS = new Set([
|
|
13248
|
+
".git",
|
|
13249
|
+
".agent",
|
|
13250
|
+
".devstation",
|
|
13251
|
+
"node_modules",
|
|
13252
|
+
".next",
|
|
13253
|
+
"dist",
|
|
13254
|
+
"build",
|
|
13255
|
+
".turbo"
|
|
13256
|
+
]);
|
|
13257
|
+
function materialise(files, root) {
|
|
13258
|
+
const workspace = new Workspace(root);
|
|
13259
|
+
const refused = [];
|
|
13260
|
+
let written = 0;
|
|
13261
|
+
for (const [path, content] of Object.entries(files)) {
|
|
13262
|
+
const result = workspace.write(path, content);
|
|
13263
|
+
if (result.ok)
|
|
13264
|
+
written++;
|
|
13265
|
+
else
|
|
13266
|
+
refused.push({ path, why: result.reason });
|
|
13267
|
+
}
|
|
13268
|
+
return { written, refused };
|
|
13269
|
+
}
|
|
13270
|
+
async function initLocalRepo(root, ref) {
|
|
13271
|
+
const result = await runShell("git init -q && git add -A && " + "git -c user.name='DevStation agent' -c user.email='agent@devstation.online' " + `commit -q -m ${JSON.stringify(`base: ${ref}`)} --allow-empty`, { cwd: root, timeoutMs: 120000 });
|
|
13272
|
+
return result.ok;
|
|
13273
|
+
}
|
|
13274
|
+
function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
13275
|
+
const files = {};
|
|
13276
|
+
const walk = (dir) => {
|
|
13277
|
+
for (const item of readdirSync2(dir, { withFileTypes: true })) {
|
|
13278
|
+
if (item.isSymbolicLink())
|
|
13279
|
+
continue;
|
|
13280
|
+
const full = join3(dir, item.name);
|
|
13281
|
+
if (item.isDirectory()) {
|
|
13282
|
+
if (SKIP_DIRS.has(item.name))
|
|
13283
|
+
continue;
|
|
13284
|
+
walk(full);
|
|
13285
|
+
continue;
|
|
13286
|
+
}
|
|
13287
|
+
if (!item.isFile())
|
|
13288
|
+
continue;
|
|
13289
|
+
if (statSync3(full).size > maxFileBytes)
|
|
13290
|
+
continue;
|
|
13291
|
+
const buffer = readFileSync3(full);
|
|
13292
|
+
if (looksBinary(buffer))
|
|
13293
|
+
continue;
|
|
13294
|
+
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
13295
|
+
}
|
|
13296
|
+
};
|
|
13297
|
+
walk(root);
|
|
13298
|
+
return files;
|
|
13299
|
+
}
|
|
13300
|
+
var INDEX_SKIP_DIRS = new Set([
|
|
13301
|
+
...SKIP_DIRS,
|
|
13302
|
+
"target",
|
|
13303
|
+
"coverage",
|
|
13304
|
+
"__pycache__",
|
|
13305
|
+
".venv",
|
|
13306
|
+
"venv",
|
|
13307
|
+
".gradle",
|
|
13308
|
+
"out"
|
|
13309
|
+
]);
|
|
13310
|
+
var HIDDEN_KEEP = new Set([".github"]);
|
|
13311
|
+
function readWorkspaceBounded(root, opts = {}) {
|
|
13312
|
+
const maxFiles = opts.maxFiles ?? 20000;
|
|
13313
|
+
const until = Date.now() + (opts.deadlineMs ?? 1e4);
|
|
13314
|
+
const maxBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
13315
|
+
const files = {};
|
|
13316
|
+
let seen = 0;
|
|
13317
|
+
let stopped = null;
|
|
13318
|
+
const walk = (dir) => {
|
|
13319
|
+
let entries;
|
|
13320
|
+
try {
|
|
13321
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
13322
|
+
} catch {
|
|
13323
|
+
return;
|
|
13324
|
+
}
|
|
13325
|
+
for (const item of entries) {
|
|
13326
|
+
if (stopped)
|
|
13327
|
+
return;
|
|
13328
|
+
if (item.isSymbolicLink())
|
|
13329
|
+
continue;
|
|
13330
|
+
const full = join3(dir, item.name);
|
|
13331
|
+
if (item.isDirectory()) {
|
|
13332
|
+
if (INDEX_SKIP_DIRS.has(item.name))
|
|
13333
|
+
continue;
|
|
13334
|
+
if (item.name.startsWith(".") && !HIDDEN_KEEP.has(item.name))
|
|
13335
|
+
continue;
|
|
13336
|
+
walk(full);
|
|
13337
|
+
continue;
|
|
13338
|
+
}
|
|
13339
|
+
if (!item.isFile())
|
|
13340
|
+
continue;
|
|
13341
|
+
if (seen >= maxFiles) {
|
|
13342
|
+
stopped = "max-files";
|
|
13343
|
+
return;
|
|
13344
|
+
}
|
|
13345
|
+
if ((seen & 63) === 0 && Date.now() > until) {
|
|
13346
|
+
stopped = "deadline";
|
|
13347
|
+
return;
|
|
13348
|
+
}
|
|
13349
|
+
seen++;
|
|
13350
|
+
try {
|
|
13351
|
+
if (statSync3(full).size > maxBytes)
|
|
13352
|
+
continue;
|
|
13353
|
+
const buffer = readFileSync3(full);
|
|
13354
|
+
if (looksBinary(buffer))
|
|
13355
|
+
continue;
|
|
13356
|
+
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
13357
|
+
} catch {}
|
|
13358
|
+
}
|
|
13359
|
+
};
|
|
13360
|
+
walk(root);
|
|
13361
|
+
return { files, stopped };
|
|
13362
|
+
}
|
|
13363
|
+
var SNAPSHOT_SKIP_DIRS = new Set([...SKIP_DIRS, ".output", ".venv"]);
|
|
13364
|
+
function listWorkspaceBounded(root, opts = {}) {
|
|
13365
|
+
const maxFiles = opts.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
13366
|
+
const maxBytes = opts.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
13367
|
+
const until = Date.now() + (opts.deadlineMs ?? 1e4);
|
|
13368
|
+
const paths = [];
|
|
13369
|
+
let bytes = 0;
|
|
13370
|
+
let stopped = null;
|
|
13371
|
+
const walk = (dir) => {
|
|
13372
|
+
let entries;
|
|
13373
|
+
try {
|
|
13374
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
13375
|
+
} catch {
|
|
13376
|
+
return;
|
|
13377
|
+
}
|
|
13378
|
+
for (const item of entries) {
|
|
13379
|
+
if (stopped)
|
|
13380
|
+
return;
|
|
13381
|
+
if (item.isSymbolicLink())
|
|
13382
|
+
continue;
|
|
13383
|
+
const full = join3(dir, item.name);
|
|
13384
|
+
if (item.isDirectory()) {
|
|
13385
|
+
if (SNAPSHOT_SKIP_DIRS.has(item.name))
|
|
13386
|
+
continue;
|
|
13387
|
+
if (opts.skipHidden && item.name.startsWith(".") && !HIDDEN_KEEP.has(item.name))
|
|
13388
|
+
continue;
|
|
13389
|
+
walk(full);
|
|
13390
|
+
continue;
|
|
13391
|
+
}
|
|
13392
|
+
if (!item.isFile())
|
|
13393
|
+
continue;
|
|
13394
|
+
if (paths.length >= maxFiles) {
|
|
13395
|
+
stopped = "max-files";
|
|
13396
|
+
return;
|
|
13397
|
+
}
|
|
13398
|
+
if ((paths.length & 63) === 0 && Date.now() > until) {
|
|
13399
|
+
stopped = "deadline";
|
|
13400
|
+
return;
|
|
13401
|
+
}
|
|
13402
|
+
try {
|
|
13403
|
+
bytes += statSync3(full).size;
|
|
13404
|
+
} catch {
|
|
13405
|
+
continue;
|
|
13406
|
+
}
|
|
13407
|
+
if (bytes > maxBytes) {
|
|
13408
|
+
stopped = "max-bytes";
|
|
13409
|
+
return;
|
|
13410
|
+
}
|
|
13411
|
+
paths.push(relative2(root, full).split(sep2).join("/"));
|
|
13412
|
+
}
|
|
13413
|
+
};
|
|
13414
|
+
walk(root);
|
|
13415
|
+
return { paths: paths.sort(), bytes, stopped };
|
|
13416
|
+
}
|
|
13417
|
+
var MAX_TITLE = 72;
|
|
13418
|
+
function proposalFor(goal, result, files) {
|
|
13419
|
+
const firstLine = (goal || result.summary).split(`
|
|
13420
|
+
`)[0].trim();
|
|
13421
|
+
const title = firstLine.length > MAX_TITLE ? `${firstLine.slice(0, MAX_TITLE - 3)}...` : firstLine;
|
|
13422
|
+
const fileList = files.length === 0 ? "_No files changed._" : files.slice(0, 50).map((f) => `- \`${f}\``).join(`
|
|
13423
|
+
`) + (files.length > 50 ? `
|
|
13424
|
+
- ...and ${files.length - 50} more` : "");
|
|
13425
|
+
const body = [
|
|
13426
|
+
`**Asked for:** ${goal}`,
|
|
13427
|
+
"",
|
|
13428
|
+
result.summary || "No summary was produced.",
|
|
13429
|
+
"",
|
|
13430
|
+
"### Files changed",
|
|
13431
|
+
fileList,
|
|
13432
|
+
"",
|
|
13433
|
+
"---",
|
|
13434
|
+
`Opened by the DevStation coding agent after ${result.steps} step(s), ` + `about $${result.costUsd.toFixed(2)}. Review it as you would any other change: ` + "nothing here has been merged, and the agent could not open this itself."
|
|
13435
|
+
].join(`
|
|
13436
|
+
`);
|
|
13437
|
+
return { title, body, message: `${title}
|
|
13438
|
+
|
|
13439
|
+
${goal}` };
|
|
13440
|
+
}
|
|
13441
|
+
|
|
13242
13442
|
// src/lib/agent/snapshots.ts
|
|
13243
13443
|
var DIR = ".devstation/checkpoints";
|
|
13244
13444
|
var KEEP = 20;
|
|
13245
13445
|
function storeDir(root) {
|
|
13246
|
-
return
|
|
13446
|
+
return join4(root, DIR);
|
|
13247
13447
|
}
|
|
13248
13448
|
function listSnapshots(root) {
|
|
13249
13449
|
const dir = storeDir(root);
|
|
13250
13450
|
if (!existsSync3(dir))
|
|
13251
13451
|
return [];
|
|
13252
13452
|
const found = [];
|
|
13253
|
-
for (const entry of
|
|
13254
|
-
const manifest =
|
|
13453
|
+
for (const entry of readdirSync3(dir)) {
|
|
13454
|
+
const manifest = join4(dir, entry, "manifest.json");
|
|
13255
13455
|
if (!existsSync3(manifest))
|
|
13256
13456
|
continue;
|
|
13257
13457
|
try {
|
|
13258
|
-
found.push(JSON.parse(
|
|
13458
|
+
found.push(JSON.parse(readFileSync4(manifest, "utf8")));
|
|
13259
13459
|
} catch {}
|
|
13260
13460
|
}
|
|
13261
13461
|
return found.sort((a, b) => b.id.localeCompare(a.id));
|
|
@@ -13267,20 +13467,36 @@ function nextId(root) {
|
|
|
13267
13467
|
}, 0);
|
|
13268
13468
|
return `${String(highest + 1).padStart(6, "0")}-${Date.now()}`;
|
|
13269
13469
|
}
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13470
|
+
var SNAPSHOT_MAX_FILES = 5000;
|
|
13471
|
+
var SNAPSHOT_MAX_BYTES = 50 * 1024 * 1024;
|
|
13472
|
+
function takeSnapshot(root, message, opts = {}) {
|
|
13473
|
+
const home = opts.home ?? process.env.HOME ?? "";
|
|
13474
|
+
const strip = (p) => p.replace(/\/+$/, "");
|
|
13475
|
+
if (home && strip(root) === strip(home)) {
|
|
13476
|
+
opts.onSkip?.("a home directory is never copied aside. cd into a project to get undo.");
|
|
13477
|
+
return null;
|
|
13478
|
+
}
|
|
13479
|
+
const listed = listWorkspaceBounded(root, {
|
|
13480
|
+
maxFiles: opts.maxFiles ?? SNAPSHOT_MAX_FILES,
|
|
13481
|
+
maxBytes: opts.maxBytes ?? SNAPSHOT_MAX_BYTES
|
|
13482
|
+
});
|
|
13483
|
+
if (listed.stopped) {
|
|
13484
|
+
const limit = listed.stopped === "max-bytes" ? `${Math.round((opts.maxBytes ?? SNAPSHOT_MAX_BYTES) / 1024 / 1024)}MB` : listed.stopped === "max-files" ? `${(opts.maxFiles ?? SNAPSHOT_MAX_FILES).toLocaleString("en-US")} files` : "the time limit";
|
|
13485
|
+
opts.onSkip?.(`this workspace is larger than ${limit}, too large to copy aside each turn. Put it under git for undo.`);
|
|
13486
|
+
return null;
|
|
13487
|
+
}
|
|
13488
|
+
const files = listed.paths.filter((p) => !p.startsWith(`${DIR}/`) && !p.startsWith(".devstation/"));
|
|
13273
13489
|
if (files.length === 0)
|
|
13274
13490
|
return null;
|
|
13275
13491
|
const id = nextId(root);
|
|
13276
|
-
const dir =
|
|
13277
|
-
mkdirSync2(
|
|
13492
|
+
const dir = join4(storeDir(root), id);
|
|
13493
|
+
mkdirSync2(join4(dir, "files"), { recursive: true });
|
|
13278
13494
|
const captured = [];
|
|
13279
13495
|
for (const path of files) {
|
|
13280
|
-
const from =
|
|
13496
|
+
const from = join4(root, path);
|
|
13281
13497
|
if (!existsSync3(from))
|
|
13282
13498
|
continue;
|
|
13283
|
-
const to =
|
|
13499
|
+
const to = join4(dir, "files", path);
|
|
13284
13500
|
mkdirSync2(dirname2(to), { recursive: true });
|
|
13285
13501
|
try {
|
|
13286
13502
|
cpSync(from, to);
|
|
@@ -13293,7 +13509,7 @@ function takeSnapshot(root, message) {
|
|
|
13293
13509
|
message,
|
|
13294
13510
|
files: captured
|
|
13295
13511
|
};
|
|
13296
|
-
writeFileSync2(
|
|
13512
|
+
writeFileSync2(join4(dir, "manifest.json"), `${JSON.stringify(snapshot, null, 2)}
|
|
13297
13513
|
`);
|
|
13298
13514
|
prune(root);
|
|
13299
13515
|
return snapshot;
|
|
@@ -13301,7 +13517,7 @@ function takeSnapshot(root, message) {
|
|
|
13301
13517
|
function prune(root) {
|
|
13302
13518
|
const all = listSnapshots(root);
|
|
13303
13519
|
for (const old of all.slice(KEEP)) {
|
|
13304
|
-
rmSync(
|
|
13520
|
+
rmSync(join4(storeDir(root), old.id), { recursive: true, force: true });
|
|
13305
13521
|
}
|
|
13306
13522
|
}
|
|
13307
13523
|
function undoSnapshot(root) {
|
|
@@ -13309,19 +13525,18 @@ function undoSnapshot(root) {
|
|
|
13309
13525
|
if (!latest) {
|
|
13310
13526
|
return { ok: false, message: "There is no checkpoint to undo." };
|
|
13311
13527
|
}
|
|
13312
|
-
const
|
|
13313
|
-
const dir = join3(storeDir(root), latest.id);
|
|
13528
|
+
const dir = join4(storeDir(root), latest.id);
|
|
13314
13529
|
const kept = new Set(latest.files);
|
|
13315
|
-
for (const path of
|
|
13530
|
+
for (const path of listWorkspaceBounded(root).paths) {
|
|
13316
13531
|
if (kept.has(path) || path.startsWith(".devstation/"))
|
|
13317
13532
|
continue;
|
|
13318
|
-
rmSync(
|
|
13533
|
+
rmSync(join4(root, path), { force: true });
|
|
13319
13534
|
}
|
|
13320
13535
|
for (const path of latest.files) {
|
|
13321
|
-
const from =
|
|
13536
|
+
const from = join4(dir, "files", path);
|
|
13322
13537
|
if (!existsSync3(from))
|
|
13323
13538
|
continue;
|
|
13324
|
-
const to =
|
|
13539
|
+
const to = join4(root, path);
|
|
13325
13540
|
mkdirSync2(dirname2(to), { recursive: true });
|
|
13326
13541
|
cpSync(from, to);
|
|
13327
13542
|
}
|
|
@@ -13329,7 +13544,7 @@ function undoSnapshot(root) {
|
|
|
13329
13544
|
return { ok: true, message: `Undid ${latest.message}` };
|
|
13330
13545
|
}
|
|
13331
13546
|
function discardSnapshot(root, id) {
|
|
13332
|
-
rmSync(
|
|
13547
|
+
rmSync(join4(storeDir(root), id), { recursive: true, force: true });
|
|
13333
13548
|
}
|
|
13334
13549
|
|
|
13335
13550
|
// src/lib/agent/policy.ts
|
|
@@ -17488,9 +17703,9 @@ var REDACTED = "[redacted]";
|
|
|
17488
17703
|
function redact(input) {
|
|
17489
17704
|
let text = input;
|
|
17490
17705
|
const kinds = new Set;
|
|
17491
|
-
text = text.replace(SECRET_KEY, (_m, key,
|
|
17706
|
+
text = text.replace(SECRET_KEY, (_m, key, sep3, q) => {
|
|
17492
17707
|
kinds.add("assignment");
|
|
17493
|
-
return `${key}${
|
|
17708
|
+
return `${key}${sep3}${q}${REDACTED}${q}`;
|
|
17494
17709
|
});
|
|
17495
17710
|
for (const { name, re } of PATTERNS) {
|
|
17496
17711
|
text = text.replace(re, () => {
|
|
@@ -17968,6 +18183,26 @@ class AnthropicProvider {
|
|
|
17968
18183
|
|
|
17969
18184
|
// src/lib/agent/providers/openrouter.ts
|
|
17970
18185
|
var OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
18186
|
+
var DEFAULT_MAX_TOKENS2 = Number(process.env.DEVSTATION_MAX_TOKENS) || 16000;
|
|
18187
|
+
var MIN_RETRY_TOKENS = 1024;
|
|
18188
|
+
function affordableTokens(detail) {
|
|
18189
|
+
const match = /can only afford (\d+)/i.exec(detail);
|
|
18190
|
+
return match ? Number(match[1]) : null;
|
|
18191
|
+
}
|
|
18192
|
+
function describeFailure(label, status, detail) {
|
|
18193
|
+
let message = detail;
|
|
18194
|
+
try {
|
|
18195
|
+
const parsed = JSON.parse(detail);
|
|
18196
|
+
if (parsed.error?.message)
|
|
18197
|
+
message = parsed.error.message;
|
|
18198
|
+
} catch {}
|
|
18199
|
+
if (status === 402 && /credit/i.test(message)) {
|
|
18200
|
+
return `${label} is out of credit for this request. Add credit at https://openrouter.ai/settings/credits, ` + "or use a cheaper model: devstation config set model anthropic/claude-sonnet-5";
|
|
18201
|
+
}
|
|
18202
|
+
if (status === 401)
|
|
18203
|
+
return `${label} rejected the API key. Run \`devstation login\` to store a new one.`;
|
|
18204
|
+
return `${label} request failed (${status}). ${message.slice(0, 300)}`;
|
|
18205
|
+
}
|
|
17971
18206
|
function stopReasonOf2(raw) {
|
|
17972
18207
|
switch (raw) {
|
|
17973
18208
|
case "stop":
|
|
@@ -18030,7 +18265,7 @@ class OpenRouterProvider {
|
|
|
18030
18265
|
}
|
|
18031
18266
|
async generate(input) {
|
|
18032
18267
|
const openRouter = this.name === "openrouter";
|
|
18033
|
-
const
|
|
18268
|
+
const send = (maxTokens) => fetch(this.endpoint, {
|
|
18034
18269
|
method: "POST",
|
|
18035
18270
|
headers: {
|
|
18036
18271
|
"content-type": "application/json",
|
|
@@ -18041,7 +18276,7 @@ class OpenRouterProvider {
|
|
|
18041
18276
|
model: this.model,
|
|
18042
18277
|
messages: toOpenAiMessages(input.system, input.messages),
|
|
18043
18278
|
stream: true,
|
|
18044
|
-
max_tokens:
|
|
18279
|
+
max_tokens: maxTokens,
|
|
18045
18280
|
temperature: 0.2,
|
|
18046
18281
|
...input.tools?.length ? {
|
|
18047
18282
|
tools: input.tools.map((t) => ({
|
|
@@ -18057,10 +18292,21 @@ class OpenRouterProvider {
|
|
|
18057
18292
|
}),
|
|
18058
18293
|
signal: input.signal
|
|
18059
18294
|
});
|
|
18295
|
+
const label = openRouter ? "OpenRouter" : `The endpoint ${this.endpoint}`;
|
|
18296
|
+
const requested = input.maxTokens ?? DEFAULT_MAX_TOKENS2;
|
|
18297
|
+
let res = await send(requested);
|
|
18298
|
+
if (res.status === 402 && openRouter) {
|
|
18299
|
+
const detail = await res.text().catch(() => "");
|
|
18300
|
+
const afford = affordableTokens(detail);
|
|
18301
|
+
if (afford !== null && afford >= MIN_RETRY_TOKENS && afford < requested) {
|
|
18302
|
+
res = await send(Math.floor(afford * 0.95));
|
|
18303
|
+
} else {
|
|
18304
|
+
throw new Error(describeFailure(label, res.status, detail));
|
|
18305
|
+
}
|
|
18306
|
+
}
|
|
18060
18307
|
if (!res.ok || !res.body) {
|
|
18061
18308
|
const detail = await res.text().catch(() => "");
|
|
18062
|
-
|
|
18063
|
-
throw new Error(`${label} request failed (${res.status}). ${detail.slice(0, 200)}`);
|
|
18309
|
+
throw new Error(describeFailure(label, res.status, detail));
|
|
18064
18310
|
}
|
|
18065
18311
|
let text = "";
|
|
18066
18312
|
let finish = null;
|
|
@@ -18133,12 +18379,12 @@ import {
|
|
|
18133
18379
|
chmodSync,
|
|
18134
18380
|
existsSync as existsSync4,
|
|
18135
18381
|
mkdirSync as mkdirSync3,
|
|
18136
|
-
readFileSync as
|
|
18382
|
+
readFileSync as readFileSync5,
|
|
18137
18383
|
renameSync,
|
|
18138
|
-
statSync as
|
|
18384
|
+
statSync as statSync4,
|
|
18139
18385
|
writeFileSync as writeFileSync3
|
|
18140
18386
|
} from "fs";
|
|
18141
|
-
import { dirname as dirname4, join as
|
|
18387
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
18142
18388
|
var PROVIDER_IDS = ["anthropic", "openrouter", "openai"];
|
|
18143
18389
|
var SETTING_KEYS = ["provider", "model", "baseUrl"];
|
|
18144
18390
|
var KEY_ENV = {
|
|
@@ -18152,16 +18398,16 @@ var DEFAULT_BASE_URL = {
|
|
|
18152
18398
|
openai: "https://api.openai.com/v1"
|
|
18153
18399
|
};
|
|
18154
18400
|
function globalDir(home) {
|
|
18155
|
-
return
|
|
18401
|
+
return join6(home, ".devstation");
|
|
18156
18402
|
}
|
|
18157
18403
|
function globalConfigPath(home) {
|
|
18158
|
-
return
|
|
18404
|
+
return join6(globalDir(home), "config.json");
|
|
18159
18405
|
}
|
|
18160
18406
|
function projectConfigPath(root) {
|
|
18161
|
-
return
|
|
18407
|
+
return join6(root, ".devstation", "config.json");
|
|
18162
18408
|
}
|
|
18163
18409
|
function credentialsPath(home) {
|
|
18164
|
-
return
|
|
18410
|
+
return join6(globalDir(home), "credentials.json");
|
|
18165
18411
|
}
|
|
18166
18412
|
function isProvider(value) {
|
|
18167
18413
|
return typeof value === "string" && PROVIDER_IDS.includes(value);
|
|
@@ -18170,7 +18416,7 @@ function readSettingsFile(path4, problems = []) {
|
|
|
18170
18416
|
if (!existsSync4(path4))
|
|
18171
18417
|
return {};
|
|
18172
18418
|
try {
|
|
18173
|
-
const raw = JSON.parse(
|
|
18419
|
+
const raw = JSON.parse(readFileSync5(path4, "utf8"));
|
|
18174
18420
|
const out = {};
|
|
18175
18421
|
if (raw.provider !== undefined) {
|
|
18176
18422
|
const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
|
|
@@ -18215,11 +18461,11 @@ function readCredentials(home, problems = []) {
|
|
|
18215
18461
|
if (!existsSync4(path4))
|
|
18216
18462
|
return {};
|
|
18217
18463
|
try {
|
|
18218
|
-
const mode =
|
|
18464
|
+
const mode = statSync4(path4).mode & 511;
|
|
18219
18465
|
if (mode & 63) {
|
|
18220
18466
|
problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
|
|
18221
18467
|
}
|
|
18222
|
-
const raw = JSON.parse(
|
|
18468
|
+
const raw = JSON.parse(readFileSync5(path4, "utf8"));
|
|
18223
18469
|
const out = {};
|
|
18224
18470
|
for (const id of PROVIDER_IDS) {
|
|
18225
18471
|
if (typeof raw[id] === "string" && raw[id].trim())
|
|
@@ -18466,150 +18712,6 @@ async function embedMissing(store, provider, options = {}) {
|
|
|
18466
18712
|
// src/lib/agent/memory/workspace-index.ts
|
|
18467
18713
|
import { join as join7 } from "path";
|
|
18468
18714
|
|
|
18469
|
-
// src/lib/agent/repo-session.ts
|
|
18470
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
18471
|
-
import { join as join6, relative as relative2, sep as sep2 } from "path";
|
|
18472
|
-
var SKIP_DIRS = new Set([
|
|
18473
|
-
".git",
|
|
18474
|
-
".agent",
|
|
18475
|
-
".devstation",
|
|
18476
|
-
"node_modules",
|
|
18477
|
-
".next",
|
|
18478
|
-
"dist",
|
|
18479
|
-
"build",
|
|
18480
|
-
".turbo"
|
|
18481
|
-
]);
|
|
18482
|
-
function materialise(files, root) {
|
|
18483
|
-
const workspace = new Workspace(root);
|
|
18484
|
-
const refused = [];
|
|
18485
|
-
let written = 0;
|
|
18486
|
-
for (const [path4, content] of Object.entries(files)) {
|
|
18487
|
-
const result = workspace.write(path4, content);
|
|
18488
|
-
if (result.ok)
|
|
18489
|
-
written++;
|
|
18490
|
-
else
|
|
18491
|
-
refused.push({ path: path4, why: result.reason });
|
|
18492
|
-
}
|
|
18493
|
-
return { written, refused };
|
|
18494
|
-
}
|
|
18495
|
-
async function initLocalRepo(root, ref) {
|
|
18496
|
-
const result = await runShell("git init -q && git add -A && " + "git -c user.name='DevStation agent' -c user.email='agent@devstation.online' " + `commit -q -m ${JSON.stringify(`base: ${ref}`)} --allow-empty`, { cwd: root, timeoutMs: 120000 });
|
|
18497
|
-
return result.ok;
|
|
18498
|
-
}
|
|
18499
|
-
function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
18500
|
-
const files = {};
|
|
18501
|
-
const walk3 = (dir) => {
|
|
18502
|
-
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
18503
|
-
if (item.isSymbolicLink())
|
|
18504
|
-
continue;
|
|
18505
|
-
const full = join6(dir, item.name);
|
|
18506
|
-
if (item.isDirectory()) {
|
|
18507
|
-
if (SKIP_DIRS.has(item.name))
|
|
18508
|
-
continue;
|
|
18509
|
-
walk3(full);
|
|
18510
|
-
continue;
|
|
18511
|
-
}
|
|
18512
|
-
if (!item.isFile())
|
|
18513
|
-
continue;
|
|
18514
|
-
if (statSync4(full).size > maxFileBytes)
|
|
18515
|
-
continue;
|
|
18516
|
-
const buffer = readFileSync5(full);
|
|
18517
|
-
if (looksBinary(buffer))
|
|
18518
|
-
continue;
|
|
18519
|
-
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
18520
|
-
}
|
|
18521
|
-
};
|
|
18522
|
-
walk3(root);
|
|
18523
|
-
return files;
|
|
18524
|
-
}
|
|
18525
|
-
var INDEX_SKIP_DIRS = new Set([
|
|
18526
|
-
...SKIP_DIRS,
|
|
18527
|
-
"target",
|
|
18528
|
-
"coverage",
|
|
18529
|
-
"__pycache__",
|
|
18530
|
-
".venv",
|
|
18531
|
-
"venv",
|
|
18532
|
-
".gradle",
|
|
18533
|
-
"out"
|
|
18534
|
-
]);
|
|
18535
|
-
var HIDDEN_KEEP = new Set([".github"]);
|
|
18536
|
-
function readWorkspaceBounded(root, opts = {}) {
|
|
18537
|
-
const maxFiles = opts.maxFiles ?? 20000;
|
|
18538
|
-
const until = Date.now() + (opts.deadlineMs ?? 1e4);
|
|
18539
|
-
const maxBytes = opts.maxFileBytes ?? 1024 * 1024;
|
|
18540
|
-
const files = {};
|
|
18541
|
-
let seen = 0;
|
|
18542
|
-
let stopped = null;
|
|
18543
|
-
const walk3 = (dir) => {
|
|
18544
|
-
let entries;
|
|
18545
|
-
try {
|
|
18546
|
-
entries = readdirSync3(dir, { withFileTypes: true });
|
|
18547
|
-
} catch {
|
|
18548
|
-
return;
|
|
18549
|
-
}
|
|
18550
|
-
for (const item of entries) {
|
|
18551
|
-
if (stopped)
|
|
18552
|
-
return;
|
|
18553
|
-
if (item.isSymbolicLink())
|
|
18554
|
-
continue;
|
|
18555
|
-
const full = join6(dir, item.name);
|
|
18556
|
-
if (item.isDirectory()) {
|
|
18557
|
-
if (INDEX_SKIP_DIRS.has(item.name))
|
|
18558
|
-
continue;
|
|
18559
|
-
if (item.name.startsWith(".") && !HIDDEN_KEEP.has(item.name))
|
|
18560
|
-
continue;
|
|
18561
|
-
walk3(full);
|
|
18562
|
-
continue;
|
|
18563
|
-
}
|
|
18564
|
-
if (!item.isFile())
|
|
18565
|
-
continue;
|
|
18566
|
-
if (seen >= maxFiles) {
|
|
18567
|
-
stopped = "max-files";
|
|
18568
|
-
return;
|
|
18569
|
-
}
|
|
18570
|
-
if ((seen & 63) === 0 && Date.now() > until) {
|
|
18571
|
-
stopped = "deadline";
|
|
18572
|
-
return;
|
|
18573
|
-
}
|
|
18574
|
-
seen++;
|
|
18575
|
-
try {
|
|
18576
|
-
if (statSync4(full).size > maxBytes)
|
|
18577
|
-
continue;
|
|
18578
|
-
const buffer = readFileSync5(full);
|
|
18579
|
-
if (looksBinary(buffer))
|
|
18580
|
-
continue;
|
|
18581
|
-
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
18582
|
-
} catch {}
|
|
18583
|
-
}
|
|
18584
|
-
};
|
|
18585
|
-
walk3(root);
|
|
18586
|
-
return { files, stopped };
|
|
18587
|
-
}
|
|
18588
|
-
var MAX_TITLE = 72;
|
|
18589
|
-
function proposalFor(goal, result, files) {
|
|
18590
|
-
const firstLine = (goal || result.summary).split(`
|
|
18591
|
-
`)[0].trim();
|
|
18592
|
-
const title = firstLine.length > MAX_TITLE ? `${firstLine.slice(0, MAX_TITLE - 3)}...` : firstLine;
|
|
18593
|
-
const fileList = files.length === 0 ? "_No files changed._" : files.slice(0, 50).map((f) => `- \`${f}\``).join(`
|
|
18594
|
-
`) + (files.length > 50 ? `
|
|
18595
|
-
- ...and ${files.length - 50} more` : "");
|
|
18596
|
-
const body = [
|
|
18597
|
-
`**Asked for:** ${goal}`,
|
|
18598
|
-
"",
|
|
18599
|
-
result.summary || "No summary was produced.",
|
|
18600
|
-
"",
|
|
18601
|
-
"### Files changed",
|
|
18602
|
-
fileList,
|
|
18603
|
-
"",
|
|
18604
|
-
"---",
|
|
18605
|
-
`Opened by the DevStation coding agent after ${result.steps} step(s), ` + `about $${result.costUsd.toFixed(2)}. Review it as you would any other change: ` + "nothing here has been merged, and the agent could not open this itself."
|
|
18606
|
-
].join(`
|
|
18607
|
-
`);
|
|
18608
|
-
return { title, body, message: `${title}
|
|
18609
|
-
|
|
18610
|
-
${goal}` };
|
|
18611
|
-
}
|
|
18612
|
-
|
|
18613
18715
|
// src/lib/agent/memory/store.ts
|
|
18614
18716
|
import { Database } from "bun:sqlite";
|
|
18615
18717
|
import { createHash } from "crypto";
|
|
@@ -19930,9 +20032,11 @@ import { readdirSync as readdirSync5 } from "fs";
|
|
|
19930
20032
|
|
|
19931
20033
|
// src/lib/agent/pricing.ts
|
|
19932
20034
|
var TABLE = [
|
|
19933
|
-
{ match: /
|
|
19934
|
-
{ match: /
|
|
19935
|
-
{ match: /
|
|
20035
|
+
{ match: /fable-5[.-]1/i, rates: { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 } },
|
|
20036
|
+
{ match: /fable/i, rates: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 } },
|
|
20037
|
+
{ match: /opus/i, rates: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
20038
|
+
{ match: /haiku/i, rates: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 } },
|
|
20039
|
+
{ match: /sonnet/i, rates: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 } }
|
|
19936
20040
|
];
|
|
19937
20041
|
function ratesFor(model, env2 = process.env) {
|
|
19938
20042
|
const override = {
|
|
@@ -20146,7 +20250,7 @@ function executeFileTool(workspace, name, args) {
|
|
|
20146
20250
|
}
|
|
20147
20251
|
|
|
20148
20252
|
// src/lib/agent/memory/retrieve.ts
|
|
20149
|
-
var
|
|
20253
|
+
var DEFAULT_MAX_TOKENS3 = 6000;
|
|
20150
20254
|
var DEFAULT_CANDIDATES = 40;
|
|
20151
20255
|
var RRF_K = 60;
|
|
20152
20256
|
var TEST_PATH = /(^|\/)(?:tests?|__tests__|spec|e2e)(\/|$)|\.(?:test|spec)\.[a-z]+$/i;
|
|
@@ -20176,7 +20280,7 @@ async function retrieve(store, query2, options = {}) {
|
|
|
20176
20280
|
score: entry.score * weightFor(entry.chunk.path),
|
|
20177
20281
|
from: [...entry.from]
|
|
20178
20282
|
})).sort((a, b) => b.score - a.score);
|
|
20179
|
-
const budget = (options.maxTokens ??
|
|
20283
|
+
const budget = (options.maxTokens ?? DEFAULT_MAX_TOKENS3) - PREAMBLE_TOKENS;
|
|
20180
20284
|
return withinBudget(ordered, budget, HEADER_TOKENS);
|
|
20181
20285
|
}
|
|
20182
20286
|
async function vectorSearch(store, query2, limit2, options) {
|
|
@@ -20459,6 +20563,7 @@ ${goal}` : goal }
|
|
|
20459
20563
|
let summary = "";
|
|
20460
20564
|
let stoppedBecause = "finished";
|
|
20461
20565
|
const usesGit = isRepo(this.opts.workspace.root);
|
|
20566
|
+
let snapshotSkip = null;
|
|
20462
20567
|
for (;; ) {
|
|
20463
20568
|
const budget = this.budgetCheck(steps, started);
|
|
20464
20569
|
if (budget) {
|
|
@@ -20492,7 +20597,15 @@ ${goal}` : goal }
|
|
|
20492
20597
|
}
|
|
20493
20598
|
messages.push({ role: "assistant", content: result.text, toolCalls: result.toolCalls });
|
|
20494
20599
|
this.progress(steps, summary, messages);
|
|
20495
|
-
|
|
20600
|
+
let pending = null;
|
|
20601
|
+
if (!usesGit && snapshotSkip === null) {
|
|
20602
|
+
pending = takeSnapshot(this.opts.workspace.root, goal.slice(0, 80), {
|
|
20603
|
+
onSkip: (reason) => {
|
|
20604
|
+
snapshotSkip = reason;
|
|
20605
|
+
this.emit("plan", `No undo for this run: ${reason}`);
|
|
20606
|
+
}
|
|
20607
|
+
});
|
|
20608
|
+
}
|
|
20496
20609
|
const turn = { applied: [], failed: [] };
|
|
20497
20610
|
for (const call of result.toolCalls) {
|
|
20498
20611
|
steps++;
|