@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.
Files changed (2) hide show
  1. package/devstation.js +303 -190
  2. 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.4";
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 readFileSync3,
13087
- readdirSync as readdirSync2,
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 join3 } from "path";
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 join3(root, DIR);
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 readdirSync2(dir)) {
13254
- const manifest = join3(dir, entry, "manifest.json");
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(readFileSync3(manifest, "utf8")));
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
- function takeSnapshot(root, message) {
13271
- const workspace = new Workspace(root);
13272
- const files = workspace.list(".").filter((p) => !p.startsWith(`${DIR}/`) && !p.startsWith(".devstation/"));
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 = join3(storeDir(root), id);
13277
- mkdirSync2(join3(dir, "files"), { recursive: true });
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 = join3(root, path);
13496
+ const from = join4(root, path);
13281
13497
  if (!existsSync3(from))
13282
13498
  continue;
13283
- const to = join3(dir, "files", path);
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(join3(dir, "manifest.json"), `${JSON.stringify(snapshot, null, 2)}
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(join3(storeDir(root), old.id), { recursive: true, force: true });
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 workspace = new Workspace(root);
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 workspace.list(".")) {
13530
+ for (const path of listWorkspaceBounded(root).paths) {
13316
13531
  if (kept.has(path) || path.startsWith(".devstation/"))
13317
13532
  continue;
13318
- rmSync(join3(root, path), { force: true });
13533
+ rmSync(join4(root, path), { force: true });
13319
13534
  }
13320
13535
  for (const path of latest.files) {
13321
- const from = join3(dir, "files", path);
13536
+ const from = join4(dir, "files", path);
13322
13537
  if (!existsSync3(from))
13323
13538
  continue;
13324
- const to = join3(root, path);
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(join3(storeDir(root), id), { recursive: true, force: true });
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, sep2, q) => {
17706
+ text = text.replace(SECRET_KEY, (_m, key, sep3, q) => {
17492
17707
  kinds.add("assignment");
17493
- return `${key}${sep2}${q}${REDACTED}${q}`;
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 res = await fetch(this.endpoint, {
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: input.maxTokens ?? 64000,
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
- const label = openRouter ? "OpenRouter" : `The endpoint ${this.endpoint}`;
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 readFileSync4,
18382
+ readFileSync as readFileSync5,
18137
18383
  renameSync,
18138
- statSync as statSync3,
18384
+ statSync as statSync4,
18139
18385
  writeFileSync as writeFileSync3
18140
18386
  } from "fs";
18141
- import { dirname as dirname4, join as join5 } from "path";
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 join5(home, ".devstation");
18401
+ return join6(home, ".devstation");
18156
18402
  }
18157
18403
  function globalConfigPath(home) {
18158
- return join5(globalDir(home), "config.json");
18404
+ return join6(globalDir(home), "config.json");
18159
18405
  }
18160
18406
  function projectConfigPath(root) {
18161
- return join5(root, ".devstation", "config.json");
18407
+ return join6(root, ".devstation", "config.json");
18162
18408
  }
18163
18409
  function credentialsPath(home) {
18164
- return join5(globalDir(home), "credentials.json");
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(readFileSync4(path4, "utf8"));
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 = statSync3(path4).mode & 511;
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(readFileSync4(path4, "utf8"));
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: /opus/i, rates: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
19934
- { match: /haiku/i, rates: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
19935
- { match: /sonnet/i, rates: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } }
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 DEFAULT_MAX_TOKENS2 = 6000;
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 ?? DEFAULT_MAX_TOKENS2) - PREAMBLE_TOKENS;
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
- const pending = usesGit ? null : takeSnapshot(this.opts.workspace.root, goal.slice(0, 80));
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++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstationlabs/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "The DevStation coding agent, in your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",