@devstationlabs/cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devstation.js +412 -167
- 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",
|
|
@@ -12731,7 +12731,7 @@ var SESSION_HELP = ` /undo rewind the last checkpoint
|
|
|
12731
12731
|
import { accessSync, constants as constants2, existsSync as existsSync10 } from "fs";
|
|
12732
12732
|
|
|
12733
12733
|
// src/lib/agent/git.ts
|
|
12734
|
-
import { existsSync } from "fs";
|
|
12734
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
12735
12735
|
import { join } from "path";
|
|
12736
12736
|
|
|
12737
12737
|
// src/lib/agent/shell.ts
|
|
@@ -12949,7 +12949,16 @@ function cloneTargetProblem(target) {
|
|
|
12949
12949
|
// src/lib/agent/git.ts
|
|
12950
12950
|
var CHECKPOINT_PREFIX = "agent checkpoint:";
|
|
12951
12951
|
function isRepo(root) {
|
|
12952
|
-
|
|
12952
|
+
const dotGit = join(root, ".git");
|
|
12953
|
+
if (!existsSync(dotGit))
|
|
12954
|
+
return false;
|
|
12955
|
+
try {
|
|
12956
|
+
if (statSync(dotGit).isFile())
|
|
12957
|
+
return readFileSync(dotGit, "utf8").startsWith("gitdir:");
|
|
12958
|
+
} catch {
|
|
12959
|
+
return false;
|
|
12960
|
+
}
|
|
12961
|
+
return existsSync(join(dotGit, "HEAD"));
|
|
12953
12962
|
}
|
|
12954
12963
|
function quote(value) {
|
|
12955
12964
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -13074,21 +13083,25 @@ import {
|
|
|
13074
13083
|
cpSync,
|
|
13075
13084
|
existsSync as existsSync3,
|
|
13076
13085
|
mkdirSync as mkdirSync2,
|
|
13077
|
-
readFileSync as
|
|
13078
|
-
readdirSync as
|
|
13086
|
+
readFileSync as readFileSync4,
|
|
13087
|
+
readdirSync as readdirSync3,
|
|
13079
13088
|
rmSync,
|
|
13080
13089
|
writeFileSync as writeFileSync2
|
|
13081
13090
|
} from "fs";
|
|
13082
|
-
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";
|
|
13083
13096
|
|
|
13084
13097
|
// src/lib/agent/workspace.ts
|
|
13085
13098
|
import {
|
|
13086
13099
|
existsSync as existsSync2,
|
|
13087
13100
|
mkdirSync,
|
|
13088
|
-
readFileSync,
|
|
13101
|
+
readFileSync as readFileSync2,
|
|
13089
13102
|
readdirSync,
|
|
13090
13103
|
realpathSync,
|
|
13091
|
-
statSync,
|
|
13104
|
+
statSync as statSync2,
|
|
13092
13105
|
writeFileSync
|
|
13093
13106
|
} from "fs";
|
|
13094
13107
|
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
@@ -13165,10 +13178,10 @@ class Workspace {
|
|
|
13165
13178
|
if (!existsSync2(resolved.absolute)) {
|
|
13166
13179
|
return { ok: false, reason: `There is no file at ${relativePath}.` };
|
|
13167
13180
|
}
|
|
13168
|
-
if (
|
|
13181
|
+
if (statSync2(resolved.absolute).isDirectory()) {
|
|
13169
13182
|
return { ok: false, reason: `${relativePath} is a directory, not a file.` };
|
|
13170
13183
|
}
|
|
13171
|
-
const buffer =
|
|
13184
|
+
const buffer = readFileSync2(resolved.absolute);
|
|
13172
13185
|
if (looksBinary(buffer)) {
|
|
13173
13186
|
return {
|
|
13174
13187
|
ok: false,
|
|
@@ -13188,10 +13201,10 @@ class Workspace {
|
|
|
13188
13201
|
if (!resolved.ok)
|
|
13189
13202
|
return { ok: false, reason: resolved.reason };
|
|
13190
13203
|
if (existsSync2(resolved.absolute)) {
|
|
13191
|
-
if (
|
|
13204
|
+
if (statSync2(resolved.absolute).isDirectory()) {
|
|
13192
13205
|
return { ok: false, reason: `${relativePath} is a directory.` };
|
|
13193
13206
|
}
|
|
13194
|
-
if (looksBinary(
|
|
13207
|
+
if (looksBinary(readFileSync2(resolved.absolute))) {
|
|
13195
13208
|
return { ok: false, reason: `${relativePath} is a binary file and was not overwritten.` };
|
|
13196
13209
|
}
|
|
13197
13210
|
}
|
|
@@ -13203,7 +13216,7 @@ class Workspace {
|
|
|
13203
13216
|
const resolved = this.resolve(relativePath);
|
|
13204
13217
|
return resolved.ok && existsSync2(resolved.absolute);
|
|
13205
13218
|
}
|
|
13206
|
-
list(subdir = ".") {
|
|
13219
|
+
list(subdir = ".", limit = Number.POSITIVE_INFINITY) {
|
|
13207
13220
|
const base = subdir === "." || subdir === "" ? { ok: true, absolute: this.root } : this.resolve(subdir);
|
|
13208
13221
|
if (!base.ok || !existsSync2(base.absolute))
|
|
13209
13222
|
return [];
|
|
@@ -13212,6 +13225,8 @@ class Workspace {
|
|
|
13212
13225
|
const out = [];
|
|
13213
13226
|
const walk = (dir) => {
|
|
13214
13227
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13228
|
+
if (out.length >= limit)
|
|
13229
|
+
return;
|
|
13215
13230
|
if (entry.name.startsWith(".") && skip.has(entry.name))
|
|
13216
13231
|
continue;
|
|
13217
13232
|
if (skip.has(entry.name))
|
|
@@ -13228,23 +13243,219 @@ class Workspace {
|
|
|
13228
13243
|
}
|
|
13229
13244
|
}
|
|
13230
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
|
+
|
|
13231
13442
|
// src/lib/agent/snapshots.ts
|
|
13232
13443
|
var DIR = ".devstation/checkpoints";
|
|
13233
13444
|
var KEEP = 20;
|
|
13234
13445
|
function storeDir(root) {
|
|
13235
|
-
return
|
|
13446
|
+
return join4(root, DIR);
|
|
13236
13447
|
}
|
|
13237
13448
|
function listSnapshots(root) {
|
|
13238
13449
|
const dir = storeDir(root);
|
|
13239
13450
|
if (!existsSync3(dir))
|
|
13240
13451
|
return [];
|
|
13241
13452
|
const found = [];
|
|
13242
|
-
for (const entry of
|
|
13243
|
-
const manifest =
|
|
13453
|
+
for (const entry of readdirSync3(dir)) {
|
|
13454
|
+
const manifest = join4(dir, entry, "manifest.json");
|
|
13244
13455
|
if (!existsSync3(manifest))
|
|
13245
13456
|
continue;
|
|
13246
13457
|
try {
|
|
13247
|
-
found.push(JSON.parse(
|
|
13458
|
+
found.push(JSON.parse(readFileSync4(manifest, "utf8")));
|
|
13248
13459
|
} catch {}
|
|
13249
13460
|
}
|
|
13250
13461
|
return found.sort((a, b) => b.id.localeCompare(a.id));
|
|
@@ -13256,20 +13467,36 @@ function nextId(root) {
|
|
|
13256
13467
|
}, 0);
|
|
13257
13468
|
return `${String(highest + 1).padStart(6, "0")}-${Date.now()}`;
|
|
13258
13469
|
}
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
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/"));
|
|
13262
13489
|
if (files.length === 0)
|
|
13263
13490
|
return null;
|
|
13264
13491
|
const id = nextId(root);
|
|
13265
|
-
const dir =
|
|
13266
|
-
mkdirSync2(
|
|
13492
|
+
const dir = join4(storeDir(root), id);
|
|
13493
|
+
mkdirSync2(join4(dir, "files"), { recursive: true });
|
|
13267
13494
|
const captured = [];
|
|
13268
13495
|
for (const path of files) {
|
|
13269
|
-
const from =
|
|
13496
|
+
const from = join4(root, path);
|
|
13270
13497
|
if (!existsSync3(from))
|
|
13271
13498
|
continue;
|
|
13272
|
-
const to =
|
|
13499
|
+
const to = join4(dir, "files", path);
|
|
13273
13500
|
mkdirSync2(dirname2(to), { recursive: true });
|
|
13274
13501
|
try {
|
|
13275
13502
|
cpSync(from, to);
|
|
@@ -13282,7 +13509,7 @@ function takeSnapshot(root, message) {
|
|
|
13282
13509
|
message,
|
|
13283
13510
|
files: captured
|
|
13284
13511
|
};
|
|
13285
|
-
writeFileSync2(
|
|
13512
|
+
writeFileSync2(join4(dir, "manifest.json"), `${JSON.stringify(snapshot, null, 2)}
|
|
13286
13513
|
`);
|
|
13287
13514
|
prune(root);
|
|
13288
13515
|
return snapshot;
|
|
@@ -13290,7 +13517,7 @@ function takeSnapshot(root, message) {
|
|
|
13290
13517
|
function prune(root) {
|
|
13291
13518
|
const all = listSnapshots(root);
|
|
13292
13519
|
for (const old of all.slice(KEEP)) {
|
|
13293
|
-
rmSync(
|
|
13520
|
+
rmSync(join4(storeDir(root), old.id), { recursive: true, force: true });
|
|
13294
13521
|
}
|
|
13295
13522
|
}
|
|
13296
13523
|
function undoSnapshot(root) {
|
|
@@ -13298,19 +13525,18 @@ function undoSnapshot(root) {
|
|
|
13298
13525
|
if (!latest) {
|
|
13299
13526
|
return { ok: false, message: "There is no checkpoint to undo." };
|
|
13300
13527
|
}
|
|
13301
|
-
const
|
|
13302
|
-
const dir = join3(storeDir(root), latest.id);
|
|
13528
|
+
const dir = join4(storeDir(root), latest.id);
|
|
13303
13529
|
const kept = new Set(latest.files);
|
|
13304
|
-
for (const path of
|
|
13530
|
+
for (const path of listWorkspaceBounded(root).paths) {
|
|
13305
13531
|
if (kept.has(path) || path.startsWith(".devstation/"))
|
|
13306
13532
|
continue;
|
|
13307
|
-
rmSync(
|
|
13533
|
+
rmSync(join4(root, path), { force: true });
|
|
13308
13534
|
}
|
|
13309
13535
|
for (const path of latest.files) {
|
|
13310
|
-
const from =
|
|
13536
|
+
const from = join4(dir, "files", path);
|
|
13311
13537
|
if (!existsSync3(from))
|
|
13312
13538
|
continue;
|
|
13313
|
-
const to =
|
|
13539
|
+
const to = join4(root, path);
|
|
13314
13540
|
mkdirSync2(dirname2(to), { recursive: true });
|
|
13315
13541
|
cpSync(from, to);
|
|
13316
13542
|
}
|
|
@@ -13318,7 +13544,7 @@ function undoSnapshot(root) {
|
|
|
13318
13544
|
return { ok: true, message: `Undid ${latest.message}` };
|
|
13319
13545
|
}
|
|
13320
13546
|
function discardSnapshot(root, id) {
|
|
13321
|
-
rmSync(
|
|
13547
|
+
rmSync(join4(storeDir(root), id), { recursive: true, force: true });
|
|
13322
13548
|
}
|
|
13323
13549
|
|
|
13324
13550
|
// src/lib/agent/policy.ts
|
|
@@ -17477,9 +17703,9 @@ var REDACTED = "[redacted]";
|
|
|
17477
17703
|
function redact(input) {
|
|
17478
17704
|
let text = input;
|
|
17479
17705
|
const kinds = new Set;
|
|
17480
|
-
text = text.replace(SECRET_KEY, (_m, key,
|
|
17706
|
+
text = text.replace(SECRET_KEY, (_m, key, sep3, q) => {
|
|
17481
17707
|
kinds.add("assignment");
|
|
17482
|
-
return `${key}${
|
|
17708
|
+
return `${key}${sep3}${q}${REDACTED}${q}`;
|
|
17483
17709
|
});
|
|
17484
17710
|
for (const { name, re } of PATTERNS) {
|
|
17485
17711
|
text = text.replace(re, () => {
|
|
@@ -17957,6 +18183,26 @@ class AnthropicProvider {
|
|
|
17957
18183
|
|
|
17958
18184
|
// src/lib/agent/providers/openrouter.ts
|
|
17959
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
|
+
}
|
|
17960
18206
|
function stopReasonOf2(raw) {
|
|
17961
18207
|
switch (raw) {
|
|
17962
18208
|
case "stop":
|
|
@@ -18019,7 +18265,7 @@ class OpenRouterProvider {
|
|
|
18019
18265
|
}
|
|
18020
18266
|
async generate(input) {
|
|
18021
18267
|
const openRouter = this.name === "openrouter";
|
|
18022
|
-
const
|
|
18268
|
+
const send = (maxTokens) => fetch(this.endpoint, {
|
|
18023
18269
|
method: "POST",
|
|
18024
18270
|
headers: {
|
|
18025
18271
|
"content-type": "application/json",
|
|
@@ -18030,7 +18276,7 @@ class OpenRouterProvider {
|
|
|
18030
18276
|
model: this.model,
|
|
18031
18277
|
messages: toOpenAiMessages(input.system, input.messages),
|
|
18032
18278
|
stream: true,
|
|
18033
|
-
max_tokens:
|
|
18279
|
+
max_tokens: maxTokens,
|
|
18034
18280
|
temperature: 0.2,
|
|
18035
18281
|
...input.tools?.length ? {
|
|
18036
18282
|
tools: input.tools.map((t) => ({
|
|
@@ -18046,10 +18292,21 @@ class OpenRouterProvider {
|
|
|
18046
18292
|
}),
|
|
18047
18293
|
signal: input.signal
|
|
18048
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
|
+
}
|
|
18049
18307
|
if (!res.ok || !res.body) {
|
|
18050
18308
|
const detail = await res.text().catch(() => "");
|
|
18051
|
-
|
|
18052
|
-
throw new Error(`${label} request failed (${res.status}). ${detail.slice(0, 200)}`);
|
|
18309
|
+
throw new Error(describeFailure(label, res.status, detail));
|
|
18053
18310
|
}
|
|
18054
18311
|
let text = "";
|
|
18055
18312
|
let finish = null;
|
|
@@ -18122,12 +18379,12 @@ import {
|
|
|
18122
18379
|
chmodSync,
|
|
18123
18380
|
existsSync as existsSync4,
|
|
18124
18381
|
mkdirSync as mkdirSync3,
|
|
18125
|
-
readFileSync as
|
|
18382
|
+
readFileSync as readFileSync5,
|
|
18126
18383
|
renameSync,
|
|
18127
|
-
statSync as
|
|
18384
|
+
statSync as statSync4,
|
|
18128
18385
|
writeFileSync as writeFileSync3
|
|
18129
18386
|
} from "fs";
|
|
18130
|
-
import { dirname as dirname4, join as
|
|
18387
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
18131
18388
|
var PROVIDER_IDS = ["anthropic", "openrouter", "openai"];
|
|
18132
18389
|
var SETTING_KEYS = ["provider", "model", "baseUrl"];
|
|
18133
18390
|
var KEY_ENV = {
|
|
@@ -18141,16 +18398,16 @@ var DEFAULT_BASE_URL = {
|
|
|
18141
18398
|
openai: "https://api.openai.com/v1"
|
|
18142
18399
|
};
|
|
18143
18400
|
function globalDir(home) {
|
|
18144
|
-
return
|
|
18401
|
+
return join6(home, ".devstation");
|
|
18145
18402
|
}
|
|
18146
18403
|
function globalConfigPath(home) {
|
|
18147
|
-
return
|
|
18404
|
+
return join6(globalDir(home), "config.json");
|
|
18148
18405
|
}
|
|
18149
18406
|
function projectConfigPath(root) {
|
|
18150
|
-
return
|
|
18407
|
+
return join6(root, ".devstation", "config.json");
|
|
18151
18408
|
}
|
|
18152
18409
|
function credentialsPath(home) {
|
|
18153
|
-
return
|
|
18410
|
+
return join6(globalDir(home), "credentials.json");
|
|
18154
18411
|
}
|
|
18155
18412
|
function isProvider(value) {
|
|
18156
18413
|
return typeof value === "string" && PROVIDER_IDS.includes(value);
|
|
@@ -18159,7 +18416,7 @@ function readSettingsFile(path4, problems = []) {
|
|
|
18159
18416
|
if (!existsSync4(path4))
|
|
18160
18417
|
return {};
|
|
18161
18418
|
try {
|
|
18162
|
-
const raw = JSON.parse(
|
|
18419
|
+
const raw = JSON.parse(readFileSync5(path4, "utf8"));
|
|
18163
18420
|
const out = {};
|
|
18164
18421
|
if (raw.provider !== undefined) {
|
|
18165
18422
|
const named = typeof raw.provider === "string" ? raw.provider.toLowerCase() : raw.provider;
|
|
@@ -18204,11 +18461,11 @@ function readCredentials(home, problems = []) {
|
|
|
18204
18461
|
if (!existsSync4(path4))
|
|
18205
18462
|
return {};
|
|
18206
18463
|
try {
|
|
18207
|
-
const mode =
|
|
18464
|
+
const mode = statSync4(path4).mode & 511;
|
|
18208
18465
|
if (mode & 63) {
|
|
18209
18466
|
problems.push(`${path4} is readable by other users (mode ${mode.toString(8)}). Fix it: chmod 600 ${path4}`);
|
|
18210
18467
|
}
|
|
18211
|
-
const raw = JSON.parse(
|
|
18468
|
+
const raw = JSON.parse(readFileSync5(path4, "utf8"));
|
|
18212
18469
|
const out = {};
|
|
18213
18470
|
for (const id of PROVIDER_IDS) {
|
|
18214
18471
|
if (typeof raw[id] === "string" && raw[id].trim())
|
|
@@ -18455,87 +18712,6 @@ async function embedMissing(store, provider, options = {}) {
|
|
|
18455
18712
|
// src/lib/agent/memory/workspace-index.ts
|
|
18456
18713
|
import { join as join7 } from "path";
|
|
18457
18714
|
|
|
18458
|
-
// src/lib/agent/repo-session.ts
|
|
18459
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
18460
|
-
import { join as join6, relative as relative2, sep as sep2 } from "path";
|
|
18461
|
-
var SKIP_DIRS = new Set([
|
|
18462
|
-
".git",
|
|
18463
|
-
".agent",
|
|
18464
|
-
".devstation",
|
|
18465
|
-
"node_modules",
|
|
18466
|
-
".next",
|
|
18467
|
-
"dist",
|
|
18468
|
-
"build",
|
|
18469
|
-
".turbo"
|
|
18470
|
-
]);
|
|
18471
|
-
function materialise(files, root) {
|
|
18472
|
-
const workspace = new Workspace(root);
|
|
18473
|
-
const refused = [];
|
|
18474
|
-
let written = 0;
|
|
18475
|
-
for (const [path4, content] of Object.entries(files)) {
|
|
18476
|
-
const result = workspace.write(path4, content);
|
|
18477
|
-
if (result.ok)
|
|
18478
|
-
written++;
|
|
18479
|
-
else
|
|
18480
|
-
refused.push({ path: path4, why: result.reason });
|
|
18481
|
-
}
|
|
18482
|
-
return { written, refused };
|
|
18483
|
-
}
|
|
18484
|
-
async function initLocalRepo(root, ref) {
|
|
18485
|
-
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 });
|
|
18486
|
-
return result.ok;
|
|
18487
|
-
}
|
|
18488
|
-
function readWorkspace(root, maxFileBytes = 1024 * 1024) {
|
|
18489
|
-
const files = {};
|
|
18490
|
-
const walk3 = (dir) => {
|
|
18491
|
-
for (const item of readdirSync3(dir, { withFileTypes: true })) {
|
|
18492
|
-
if (item.isSymbolicLink())
|
|
18493
|
-
continue;
|
|
18494
|
-
const full = join6(dir, item.name);
|
|
18495
|
-
if (item.isDirectory()) {
|
|
18496
|
-
if (SKIP_DIRS.has(item.name))
|
|
18497
|
-
continue;
|
|
18498
|
-
walk3(full);
|
|
18499
|
-
continue;
|
|
18500
|
-
}
|
|
18501
|
-
if (!item.isFile())
|
|
18502
|
-
continue;
|
|
18503
|
-
if (statSync3(full).size > maxFileBytes)
|
|
18504
|
-
continue;
|
|
18505
|
-
const buffer = readFileSync4(full);
|
|
18506
|
-
if (looksBinary(buffer))
|
|
18507
|
-
continue;
|
|
18508
|
-
files[relative2(root, full).split(sep2).join("/")] = buffer.toString("utf8");
|
|
18509
|
-
}
|
|
18510
|
-
};
|
|
18511
|
-
walk3(root);
|
|
18512
|
-
return files;
|
|
18513
|
-
}
|
|
18514
|
-
var MAX_TITLE = 72;
|
|
18515
|
-
function proposalFor(goal, result, files) {
|
|
18516
|
-
const firstLine = (goal || result.summary).split(`
|
|
18517
|
-
`)[0].trim();
|
|
18518
|
-
const title = firstLine.length > MAX_TITLE ? `${firstLine.slice(0, MAX_TITLE - 3)}...` : firstLine;
|
|
18519
|
-
const fileList = files.length === 0 ? "_No files changed._" : files.slice(0, 50).map((f) => `- \`${f}\``).join(`
|
|
18520
|
-
`) + (files.length > 50 ? `
|
|
18521
|
-
- ...and ${files.length - 50} more` : "");
|
|
18522
|
-
const body = [
|
|
18523
|
-
`**Asked for:** ${goal}`,
|
|
18524
|
-
"",
|
|
18525
|
-
result.summary || "No summary was produced.",
|
|
18526
|
-
"",
|
|
18527
|
-
"### Files changed",
|
|
18528
|
-
fileList,
|
|
18529
|
-
"",
|
|
18530
|
-
"---",
|
|
18531
|
-
`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."
|
|
18532
|
-
].join(`
|
|
18533
|
-
`);
|
|
18534
|
-
return { title, body, message: `${title}
|
|
18535
|
-
|
|
18536
|
-
${goal}` };
|
|
18537
|
-
}
|
|
18538
|
-
|
|
18539
18715
|
// src/lib/agent/memory/store.ts
|
|
18540
18716
|
import { Database } from "bun:sqlite";
|
|
18541
18717
|
import { createHash } from "crypto";
|
|
@@ -18879,7 +19055,7 @@ class MemoryStore {
|
|
|
18879
19055
|
this.db.run("DELETE FROM chunks WHERE path = ?", [path4]);
|
|
18880
19056
|
this.db.run("DELETE FROM files WHERE path = ?", [path4]);
|
|
18881
19057
|
}
|
|
18882
|
-
reindex(files) {
|
|
19058
|
+
reindex(files, options = {}) {
|
|
18883
19059
|
const result = { scanned: 0, reindexed: 0, removed: 0, chunks: 0 };
|
|
18884
19060
|
this.db.run("BEGIN");
|
|
18885
19061
|
try {
|
|
@@ -18892,9 +19068,11 @@ class MemoryStore {
|
|
|
18892
19068
|
result.chunks += this.replaceFile(path4, content);
|
|
18893
19069
|
result.reindexed++;
|
|
18894
19070
|
}
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
19071
|
+
if (!options.partial) {
|
|
19072
|
+
for (const gone of known) {
|
|
19073
|
+
this.removeFile(gone);
|
|
19074
|
+
result.removed++;
|
|
19075
|
+
}
|
|
18898
19076
|
}
|
|
18899
19077
|
this.db.run("COMMIT");
|
|
18900
19078
|
} catch (error2) {
|
|
@@ -18979,8 +19157,27 @@ function openStore(root) {
|
|
|
18979
19157
|
return new MemoryStore(storePath(root));
|
|
18980
19158
|
}
|
|
18981
19159
|
async function indexWorkspace(root, options = {}) {
|
|
19160
|
+
const started = Date.now();
|
|
19161
|
+
const home = options.home ?? process.env.HOME ?? "";
|
|
19162
|
+
const strip2 = (p) => p.replace(/\/+$/, "");
|
|
19163
|
+
if (home && strip2(root) === strip2(home)) {
|
|
19164
|
+
return {
|
|
19165
|
+
scanned: 0,
|
|
19166
|
+
reindexed: 0,
|
|
19167
|
+
removed: 0,
|
|
19168
|
+
chunks: 0,
|
|
19169
|
+
embedded: 0,
|
|
19170
|
+
embeddingModel: null,
|
|
19171
|
+
stopped: "home",
|
|
19172
|
+
ms: 0
|
|
19173
|
+
};
|
|
19174
|
+
}
|
|
18982
19175
|
const store = options.store ?? openStore(root);
|
|
18983
|
-
const
|
|
19176
|
+
const read = readWorkspaceBounded(root, {
|
|
19177
|
+
maxFiles: options.maxFiles,
|
|
19178
|
+
deadlineMs: options.deadlineMs
|
|
19179
|
+
});
|
|
19180
|
+
const result = store.reindex(read.files, { partial: read.stopped !== null });
|
|
18984
19181
|
let embedded = 0;
|
|
18985
19182
|
if (options.embeddings) {
|
|
18986
19183
|
try {
|
|
@@ -18989,11 +19186,17 @@ async function indexWorkspace(root, options = {}) {
|
|
|
18989
19186
|
embedded = 0;
|
|
18990
19187
|
}
|
|
18991
19188
|
}
|
|
18992
|
-
return {
|
|
19189
|
+
return {
|
|
19190
|
+
...result,
|
|
19191
|
+
embedded,
|
|
19192
|
+
embeddingModel: options.embeddings?.model ?? null,
|
|
19193
|
+
stopped: read.stopped,
|
|
19194
|
+
ms: Date.now() - started
|
|
19195
|
+
};
|
|
18993
19196
|
}
|
|
18994
19197
|
|
|
18995
19198
|
// src/lib/agent/memory/project-memory.ts
|
|
18996
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as
|
|
19199
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
18997
19200
|
import { dirname as dirname6, join as join8 } from "path";
|
|
18998
19201
|
var DEFAULT_FILE = "PROJECT_MEMORY.md";
|
|
18999
19202
|
var HEADER = `# Project memory
|
|
@@ -19012,7 +19215,7 @@ function readMemory(root) {
|
|
|
19012
19215
|
const path4 = memoryPath(root);
|
|
19013
19216
|
if (!existsSync5(path4))
|
|
19014
19217
|
return [];
|
|
19015
|
-
return parseMemory(
|
|
19218
|
+
return parseMemory(readFileSync6(path4, "utf8"));
|
|
19016
19219
|
}
|
|
19017
19220
|
var ENTRY = /^- \[([^\]]+)\](?:\s*\(([^)]*)\))?\s+([\s\S]*)$/;
|
|
19018
19221
|
function parseMemory(text) {
|
|
@@ -19036,7 +19239,7 @@ function remember(root, note, tag = null, now = new Date) {
|
|
|
19036
19239
|
if (!text) {
|
|
19037
19240
|
return { ok: false, path: path4, message: "There was nothing to remember." };
|
|
19038
19241
|
}
|
|
19039
|
-
const existing = existsSync5(path4) ?
|
|
19242
|
+
const existing = existsSync5(path4) ? readFileSync6(path4, "utf8") : "";
|
|
19040
19243
|
const entries = parseMemory(existing);
|
|
19041
19244
|
const normal = (value) => value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
19042
19245
|
if (entries.some((entry) => normal(entry.note) === normal(text))) {
|
|
@@ -19072,7 +19275,7 @@ function renderMemory(entries) {
|
|
|
19072
19275
|
|
|
19073
19276
|
// src/lib/agent/mcp.ts
|
|
19074
19277
|
import { spawn as spawn3 } from "child_process";
|
|
19075
|
-
import { existsSync as existsSync6, readFileSync as
|
|
19278
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
19076
19279
|
import { join as join9 } from "path";
|
|
19077
19280
|
function configPaths(root, home = process.env.HOME ?? "") {
|
|
19078
19281
|
return [
|
|
@@ -19087,7 +19290,7 @@ function loadConfig(root, home = process.env.HOME ?? "") {
|
|
|
19087
19290
|
if (!existsSync6(path4))
|
|
19088
19291
|
continue;
|
|
19089
19292
|
try {
|
|
19090
|
-
const parsed = JSON.parse(
|
|
19293
|
+
const parsed = JSON.parse(readFileSync7(path4, "utf8"));
|
|
19091
19294
|
for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
|
|
19092
19295
|
if (server && typeof server.command === "string")
|
|
19093
19296
|
merged.mcpServers[name] = server;
|
|
@@ -19347,11 +19550,11 @@ function failedResult(message) {
|
|
|
19347
19550
|
|
|
19348
19551
|
// src/lib/agent/sandbox-exec.ts
|
|
19349
19552
|
import { randomBytes } from "crypto";
|
|
19350
|
-
import { existsSync as existsSync8, statSync as
|
|
19553
|
+
import { existsSync as existsSync8, statSync as statSync6, unlinkSync } from "fs";
|
|
19351
19554
|
import { isAbsolute as isAbsolute3, join as join11, relative as relative4, resolve as resolve3, sep as sep4 } from "path";
|
|
19352
19555
|
|
|
19353
19556
|
// src/lib/agent/project.ts
|
|
19354
|
-
import { existsSync as existsSync7, readFileSync as
|
|
19557
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
19355
19558
|
import { dirname as dirname7, join as join10, relative as relative3, sep as sep3 } from "path";
|
|
19356
19559
|
var SKIP = new Set([
|
|
19357
19560
|
"node_modules",
|
|
@@ -19378,7 +19581,7 @@ var MANIFESTS = [
|
|
|
19378
19581
|
var MAX_DEPTH = 2;
|
|
19379
19582
|
function readScripts(absolute) {
|
|
19380
19583
|
try {
|
|
19381
|
-
const parsed = JSON.parse(
|
|
19584
|
+
const parsed = JSON.parse(readFileSync8(absolute, "utf8"));
|
|
19382
19585
|
return parsed.scripts ?? {};
|
|
19383
19586
|
} catch {
|
|
19384
19587
|
return {};
|
|
@@ -19411,7 +19614,7 @@ function detectManifests(root) {
|
|
|
19411
19614
|
continue;
|
|
19412
19615
|
const child = join10(dir, entry);
|
|
19413
19616
|
try {
|
|
19414
|
-
if (
|
|
19617
|
+
if (statSync5(child).isDirectory())
|
|
19415
19618
|
scan(child, depth + 1);
|
|
19416
19619
|
} catch {}
|
|
19417
19620
|
}
|
|
@@ -19685,7 +19888,7 @@ function sandboxExecutor(options) {
|
|
|
19685
19888
|
return "The sandbox could not write to the workspace. Check the mount and try --no-sandbox.";
|
|
19686
19889
|
}
|
|
19687
19890
|
try {
|
|
19688
|
-
const stat2 =
|
|
19891
|
+
const stat2 = statSync6(path4);
|
|
19689
19892
|
const [uid, gid] = user.split(":").map(Number);
|
|
19690
19893
|
if (stat2.uid !== uid || stat2.gid !== gid) {
|
|
19691
19894
|
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.";
|
|
@@ -19829,9 +20032,11 @@ import { readdirSync as readdirSync5 } from "fs";
|
|
|
19829
20032
|
|
|
19830
20033
|
// src/lib/agent/pricing.ts
|
|
19831
20034
|
var TABLE = [
|
|
19832
|
-
{ match: /
|
|
19833
|
-
{ match: /
|
|
19834
|
-
{ 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 } }
|
|
19835
20040
|
];
|
|
19836
20041
|
function ratesFor(model, env2 = process.env) {
|
|
19837
20042
|
const override = {
|
|
@@ -19953,8 +20158,20 @@ function fail(reason) {
|
|
|
19953
20158
|
function executeFileTool(workspace, name, args) {
|
|
19954
20159
|
switch (name) {
|
|
19955
20160
|
case "list_files": {
|
|
19956
|
-
const
|
|
19957
|
-
|
|
20161
|
+
const LIMIT = 2000;
|
|
20162
|
+
const files = workspace.list(String(args.path ?? "."), LIMIT + 1);
|
|
20163
|
+
if (files.length === 0)
|
|
20164
|
+
return { ok: true, output: "The workspace is empty." };
|
|
20165
|
+
if (files.length > LIMIT) {
|
|
20166
|
+
return {
|
|
20167
|
+
ok: true,
|
|
20168
|
+
output: `${files.slice(0, LIMIT).join(`
|
|
20169
|
+
`)}
|
|
20170
|
+
|
|
20171
|
+
` + `(Stopped at ${LIMIT} files: this directory is very large. List a subdirectory, or search for what you need.)`
|
|
20172
|
+
};
|
|
20173
|
+
}
|
|
20174
|
+
return { ok: true, output: files.join(`
|
|
19958
20175
|
`) };
|
|
19959
20176
|
}
|
|
19960
20177
|
case "read_file": {
|
|
@@ -19991,7 +20208,10 @@ function executeFileTool(workspace, name, args) {
|
|
|
19991
20208
|
return fail("No search query was given.");
|
|
19992
20209
|
const hits = [];
|
|
19993
20210
|
let truncated = false;
|
|
19994
|
-
|
|
20211
|
+
const MAX_SCAN = 20000;
|
|
20212
|
+
const scanned = workspace.list(".", MAX_SCAN + 1);
|
|
20213
|
+
const scanLimited = scanned.length > MAX_SCAN;
|
|
20214
|
+
for (const path4 of scanned.slice(0, MAX_SCAN)) {
|
|
19995
20215
|
const file = workspace.read(path4);
|
|
19996
20216
|
if (!file.ok)
|
|
19997
20217
|
continue;
|
|
@@ -20010,14 +20230,18 @@ function executeFileTool(workspace, name, args) {
|
|
|
20010
20230
|
if (truncated)
|
|
20011
20231
|
break;
|
|
20012
20232
|
}
|
|
20013
|
-
|
|
20014
|
-
|
|
20233
|
+
const scanNote = scanLimited ? `
|
|
20234
|
+
|
|
20235
|
+
(Searched the first ${MAX_SCAN} files only: this workspace is very large. Search a subdirectory.)` : "";
|
|
20236
|
+
if (hits.length === 0) {
|
|
20237
|
+
return { ok: true, output: `No match for "${args.query}".${scanNote}` };
|
|
20238
|
+
}
|
|
20015
20239
|
return {
|
|
20016
20240
|
ok: true,
|
|
20017
|
-
output: truncated ? `${hits.join(`
|
|
20241
|
+
output: `${truncated ? `${hits.join(`
|
|
20018
20242
|
`)}
|
|
20019
20243
|
\u2026 more matches not shown` : hits.join(`
|
|
20020
|
-
`)
|
|
20244
|
+
`)}${scanNote}`
|
|
20021
20245
|
};
|
|
20022
20246
|
}
|
|
20023
20247
|
default:
|
|
@@ -20026,7 +20250,7 @@ function executeFileTool(workspace, name, args) {
|
|
|
20026
20250
|
}
|
|
20027
20251
|
|
|
20028
20252
|
// src/lib/agent/memory/retrieve.ts
|
|
20029
|
-
var
|
|
20253
|
+
var DEFAULT_MAX_TOKENS3 = 6000;
|
|
20030
20254
|
var DEFAULT_CANDIDATES = 40;
|
|
20031
20255
|
var RRF_K = 60;
|
|
20032
20256
|
var TEST_PATH = /(^|\/)(?:tests?|__tests__|spec|e2e)(\/|$)|\.(?:test|spec)\.[a-z]+$/i;
|
|
@@ -20056,7 +20280,7 @@ async function retrieve(store, query2, options = {}) {
|
|
|
20056
20280
|
score: entry.score * weightFor(entry.chunk.path),
|
|
20057
20281
|
from: [...entry.from]
|
|
20058
20282
|
})).sort((a, b) => b.score - a.score);
|
|
20059
|
-
const budget = (options.maxTokens ??
|
|
20283
|
+
const budget = (options.maxTokens ?? DEFAULT_MAX_TOKENS3) - PREAMBLE_TOKENS;
|
|
20060
20284
|
return withinBudget(ordered, budget, HEADER_TOKENS);
|
|
20061
20285
|
}
|
|
20062
20286
|
async function vectorSearch(store, query2, limit2, options) {
|
|
@@ -20339,6 +20563,7 @@ ${goal}` : goal }
|
|
|
20339
20563
|
let summary = "";
|
|
20340
20564
|
let stoppedBecause = "finished";
|
|
20341
20565
|
const usesGit = isRepo(this.opts.workspace.root);
|
|
20566
|
+
let snapshotSkip = null;
|
|
20342
20567
|
for (;; ) {
|
|
20343
20568
|
const budget = this.budgetCheck(steps, started);
|
|
20344
20569
|
if (budget) {
|
|
@@ -20372,7 +20597,15 @@ ${goal}` : goal }
|
|
|
20372
20597
|
}
|
|
20373
20598
|
messages.push({ role: "assistant", content: result.text, toolCalls: result.toolCalls });
|
|
20374
20599
|
this.progress(steps, summary, messages);
|
|
20375
|
-
|
|
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
|
+
}
|
|
20376
20609
|
const turn = { applied: [], failed: [] };
|
|
20377
20610
|
for (const call of result.toolCalls) {
|
|
20378
20611
|
steps++;
|
|
@@ -20738,10 +20971,10 @@ import {
|
|
|
20738
20971
|
appendFileSync,
|
|
20739
20972
|
existsSync as existsSync9,
|
|
20740
20973
|
mkdirSync as mkdirSync6,
|
|
20741
|
-
readFileSync as
|
|
20974
|
+
readFileSync as readFileSync9,
|
|
20742
20975
|
readdirSync as readdirSync6,
|
|
20743
20976
|
renameSync as renameSync2,
|
|
20744
|
-
statSync as
|
|
20977
|
+
statSync as statSync7,
|
|
20745
20978
|
writeFileSync as writeFileSync5
|
|
20746
20979
|
} from "fs";
|
|
20747
20980
|
import { join as join12 } from "path";
|
|
@@ -20804,7 +21037,7 @@ class SessionStore {
|
|
|
20804
21037
|
if (!existsSync9(path4))
|
|
20805
21038
|
return null;
|
|
20806
21039
|
try {
|
|
20807
|
-
return JSON.parse(
|
|
21040
|
+
return JSON.parse(readFileSync9(path4, "utf8"));
|
|
20808
21041
|
} catch {
|
|
20809
21042
|
return null;
|
|
20810
21043
|
}
|
|
@@ -20826,10 +21059,10 @@ class SessionStore {
|
|
|
20826
21059
|
const path4 = this.eventPath(id);
|
|
20827
21060
|
if (!existsSync9(path4))
|
|
20828
21061
|
return { events: [], offset: 0 };
|
|
20829
|
-
const size =
|
|
21062
|
+
const size = statSync7(path4).size;
|
|
20830
21063
|
if (size <= fromByte)
|
|
20831
21064
|
return { events: [], offset: size };
|
|
20832
|
-
const text =
|
|
21065
|
+
const text = readFileSync9(path4, "utf8").slice(fromByte);
|
|
20833
21066
|
const events = [];
|
|
20834
21067
|
let consumed = 0;
|
|
20835
21068
|
for (const line of text.split(`
|
|
@@ -20973,9 +21206,21 @@ async function runCommand(context, goal, options = {}) {
|
|
|
20973
21206
|
executor = built.executor;
|
|
20974
21207
|
}
|
|
20975
21208
|
const embeddings = embeddingsFromEnv();
|
|
21209
|
+
const firstIndex = !existsSync10(storePath(context.root));
|
|
21210
|
+
if (firstIndex && !options.quiet)
|
|
21211
|
+
terminal.out("Indexing the workspace so the agent can search it\u2026");
|
|
20976
21212
|
const memory = openStore(context.root);
|
|
20977
21213
|
try {
|
|
20978
|
-
await indexWorkspace(context.root, { store: memory, embeddings });
|
|
21214
|
+
const indexed = await indexWorkspace(context.root, { store: memory, embeddings });
|
|
21215
|
+
if (!options.quiet) {
|
|
21216
|
+
if (indexed.stopped === "home") {
|
|
21217
|
+
terminal.err("Not indexing your home directory. The agent can still read and list files; cd into a project for a real index.");
|
|
21218
|
+
} else if (indexed.stopped) {
|
|
21219
|
+
terminal.err(`Indexed ${indexed.scanned} files and stopped at the ${indexed.stopped === "deadline" ? "time" : "file"} limit: this workspace is very large. cd into the project you mean.`);
|
|
21220
|
+
} else if (firstIndex || indexed.ms > 1500) {
|
|
21221
|
+
terminal.out(`Indexed ${indexed.scanned} files in ${(indexed.ms / 1000).toFixed(1)}s.`);
|
|
21222
|
+
}
|
|
21223
|
+
}
|
|
20979
21224
|
} catch {}
|
|
20980
21225
|
const mcp = await McpHub.start(loadConfig(context.root));
|
|
20981
21226
|
for (const status of mcp.status) {
|
|
@@ -22160,7 +22405,7 @@ function lineReader(rl, write) {
|
|
|
22160
22405
|
// src/lib/agent/cli/upgrade.ts
|
|
22161
22406
|
import { spawnSync } from "child_process";
|
|
22162
22407
|
import { createHash as createHash2 } from "crypto";
|
|
22163
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as
|
|
22408
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
22164
22409
|
import { dirname as dirname8, join as join14 } from "path";
|
|
22165
22410
|
var PACKAGE = "@devstationlabs/cli";
|
|
22166
22411
|
var REPO = "linoxbt/dev-shipyard";
|
|
@@ -22301,7 +22546,7 @@ function notifyIfOutdated(terminal, opts = {}) {
|
|
|
22301
22546
|
const path4 = join14(homeDir, ".devstation", "update-check.json");
|
|
22302
22547
|
let cached = {};
|
|
22303
22548
|
try {
|
|
22304
|
-
cached = JSON.parse(
|
|
22549
|
+
cached = JSON.parse(readFileSync10(path4, "utf8"));
|
|
22305
22550
|
} catch {}
|
|
22306
22551
|
if (cached.latest && compareVersions(cached.latest, VERSION) > 0) {
|
|
22307
22552
|
terminal.err(`devstation ${cached.latest} is available (you have ${VERSION}). Run: devstation upgrade`);
|