steward-cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +1318 -645
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -44696,15 +44696,6 @@ function wrapVisualLineWithCursor(text, maxCols, targetCharOffset, hangingIndent
44696
44696
  }
44697
44697
  function emitCurrentLine() {
44698
44698
  const activeStyles = getActiveStyleCodes();
44699
- if (activeBg && currentWidth < maxCols) {
44700
- const remainingCols = maxCols - currentWidth;
44701
- currentTokens.push({
44702
- type: "char",
44703
- value: " ".repeat(remainingCols),
44704
- width: remainingCols
44705
- });
44706
- currentWidth = maxCols;
44707
- }
44708
44699
  let lineStr = currentTokens.map((t) => t.value).join("");
44709
44700
  if (activeStyles.length > 0 && !lineStr.endsWith("\x1B[0m")) {
44710
44701
  lineStr += "\x1B[0m";
@@ -52404,6 +52395,8 @@ function formatToolStatus(options) {
52404
52395
  let mainLine = `${bullet} ${dispName}`;
52405
52396
  if (truncatedArg) {
52406
52397
  mainLine += `${source_default.dim("(")}${source_default.dim(truncatedArg)}${source_default.dim(")")}`;
52398
+ } else {
52399
+ mainLine += `${source_default.dim("()")}`;
52407
52400
  }
52408
52401
  const lines = [mainLine];
52409
52402
  if (status === "completed" && options.toolOutput) {
@@ -52415,23 +52408,12 @@ function formatToolStatus(options) {
52415
52408
  const maxOutLen = Math.max(10, fullTermWidth - 6);
52416
52409
  const truncatedSummary = summaryText.length > maxOutLen ? `${summaryText.slice(0, maxOutLen - 1)}…` : summaryText;
52417
52410
  lines.push(` ${source_default.dim("└ ")}${source_default.white(truncatedSummary)}`);
52418
- const diffDeleteBg = themeBgColor(theme.diffDeleteBG);
52419
- const diffAddBg = themeBgColor(theme.diffAddBG);
52420
- const rawDiffDelBg = theme.diffDeleteBG;
52421
- const rawDiffAddBg = theme.diffAddBG;
52411
+ const maxContentWidth = Math.max(30, Math.min(fullTermWidth - 4, Math.floor(fullTermWidth * 0.85)));
52422
52412
  for (let i = 1;i < outputLines.length; i++) {
52423
- const line = outputLines[i] ?? "";
52424
- const plain = stripAnsi(line);
52425
- const isDelete = line.includes(rawDiffDelBg) || /\d+\s+-/.test(plain) || plain.startsWith("-");
52426
- const isAdd = line.includes(rawDiffAddBg) || /\d+\s+\+/.test(plain) || plain.startsWith("+");
52427
- if (isDelete || isAdd) {
52428
- const bgFn = isDelete ? diffDeleteBg : diffAddBg;
52429
- const visLen = 3 + stringWidth(plain);
52430
- const padLen = Math.max(0, fullTermWidth - visLen);
52431
- lines.push(bgFn(` ${line}${" ".repeat(padLen)}`));
52432
- } else {
52433
- lines.push(` ${line}`);
52434
- }
52413
+ const rawLine = outputLines[i] ?? "";
52414
+ const prefixed = ` ${rawLine}`;
52415
+ const wrapped = wrapVisualLine(prefixed, maxContentWidth, " ");
52416
+ lines.push(...wrapped);
52435
52417
  }
52436
52418
  }
52437
52419
  }
@@ -52991,7 +52973,7 @@ class TerminalEngine {
52991
52973
  }
52992
52974
 
52993
52975
  // src/engine/agent-session.ts
52994
- import { randomUUID as randomUUID7 } from "node:crypto";
52976
+ import { randomUUID as randomUUID8 } from "node:crypto";
52995
52977
  // src/session/helpers.ts
52996
52978
  function formatToolOutputSummary(res, isError = false) {
52997
52979
  if (isError || res === undefined || res === null) {
@@ -70973,7 +70955,6 @@ class ToolCatalog {
70973
70955
  for (const [name, def] of this.tools.entries()) {
70974
70956
  aiTools[name] = tool({
70975
70957
  description: def.description,
70976
- inputSchema: def.parameters,
70977
70958
  parameters: def.parameters,
70978
70959
  execute: async (args) => {
70979
70960
  return def.execute(args, context);
@@ -71099,7 +71080,8 @@ var writeFileTool = {
71099
71080
  summarize: (args, result) => {
71100
71081
  const lines = result?.linesWritten ?? 0;
71101
71082
  const filePath = args.file_path;
71102
- const summary = `Wrote ${lines} line${lines === 1 ? "" : "s"} to ${filePath}`;
71083
+ const isNew = result?.isNew ?? true;
71084
+ const summary = isNew ? `Created ${filePath} (${lines} line${lines === 1 ? "" : "s"})` : `Wrote ${lines} line${lines === 1 ? "" : "s"} to ${filePath}`;
71103
71085
  const contentLines = args.content.split(/\r?\n/);
71104
71086
  if (contentLines.length === 0 || contentLines.length === 1 && !contentLines[0]) {
71105
71087
  return summary;
@@ -71107,7 +71089,7 @@ var writeFileTool = {
71107
71089
  const cap = 50;
71108
71090
  const shown = contentLines.slice(0, cap);
71109
71091
  const padWidth = String(contentLines.length).length;
71110
- const detail = shown.map((l, i) => `${String(i + 1).padStart(padWidth)} ${l}`).join(`
71092
+ const detail = shown.map((l, i) => `${source_default.dim(String(i + 1).padStart(padWidth))} ${source_default.white(l)}`).join(`
71111
71093
  `);
71112
71094
  const overflow = contentLines.length > cap ? `
71113
71095
  … (${contentLines.length - cap} more lines)` : "";
@@ -71233,6 +71215,237 @@ import {
71233
71215
  writeSync as writeSync6
71234
71216
  } from "node:fs";
71235
71217
  import { dirname as dirname6, join as join8 } from "node:path";
71218
+
71219
+ // src/utils/diff.ts
71220
+ function tokenizeLines(content) {
71221
+ if (!content)
71222
+ return [];
71223
+ const tokens = [];
71224
+ const regex = /([^\r\n]*)(\r?\n)?/g;
71225
+ let match;
71226
+ while ((match = regex.exec(content)) !== null) {
71227
+ if (match.index === content.length)
71228
+ break;
71229
+ const text = match[1] ?? "";
71230
+ const ending = match[2] ?? "";
71231
+ tokens.push({ text, ending });
71232
+ if (!ending && match.index + text.length === content.length)
71233
+ break;
71234
+ }
71235
+ return tokens;
71236
+ }
71237
+ function tokenizeWords(text) {
71238
+ if (!text)
71239
+ return [];
71240
+ const matches = text.match(/[\w]+|[^\w\s]+|\s+/g);
71241
+ return matches ?? [text];
71242
+ }
71243
+ function myersDiff(oldArr, newArr, equals = (a, b) => a === b) {
71244
+ const n = oldArr.length;
71245
+ const m = newArr.length;
71246
+ if (n === 0 && m === 0)
71247
+ return [];
71248
+ if (n === 0)
71249
+ return newArr.map((newItem, i) => ({ kind: "insert", newItem, newIndex: i }));
71250
+ if (m === 0)
71251
+ return oldArr.map((oldItem, i) => ({ kind: "delete", oldItem, oldIndex: i }));
71252
+ const max = n + m;
71253
+ const v = { 1: 0 };
71254
+ const trace = [];
71255
+ for (let d = 0;d <= max; d++) {
71256
+ trace.push({ ...v });
71257
+ for (let k = -d;k <= d; k += 2) {
71258
+ let x = k === -d || k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1) ? v[k + 1] ?? 0 : (v[k - 1] ?? 0) + 1;
71259
+ let y = x - k;
71260
+ while (x < n && y < m && equals(oldArr[x], newArr[y])) {
71261
+ x++;
71262
+ y++;
71263
+ }
71264
+ v[k] = x;
71265
+ if (x >= n && y >= m) {
71266
+ const ops = [];
71267
+ let cx = n;
71268
+ let cy = m;
71269
+ for (let cd = d;cd > 0; cd--) {
71270
+ const prevV = trace[cd];
71271
+ const pk = cx - cy;
71272
+ const prevK = pk === -cd || pk !== cd && (prevV[pk - 1] ?? -1) < (prevV[pk + 1] ?? -1) ? pk + 1 : pk - 1;
71273
+ const prevX = prevV[prevK] ?? 0;
71274
+ const prevY = prevX - prevK;
71275
+ while (cx > prevX + (prevK === pk + 1 ? 0 : 1) && cy > prevY + (prevK === pk + 1 ? 1 : 0)) {
71276
+ cx--;
71277
+ cy--;
71278
+ ops.push({
71279
+ kind: "equal",
71280
+ oldItem: oldArr[cx],
71281
+ newItem: newArr[cy],
71282
+ oldIndex: cx,
71283
+ newIndex: cy
71284
+ });
71285
+ }
71286
+ if (prevK === pk + 1) {
71287
+ cy--;
71288
+ ops.push({ kind: "insert", newItem: newArr[cy], newIndex: cy });
71289
+ } else {
71290
+ cx--;
71291
+ ops.push({ kind: "delete", oldItem: oldArr[cx], oldIndex: cx });
71292
+ }
71293
+ }
71294
+ while (cx > 0 && cy > 0) {
71295
+ cx--;
71296
+ cy--;
71297
+ ops.push({
71298
+ kind: "equal",
71299
+ oldItem: oldArr[cx],
71300
+ newItem: newArr[cy],
71301
+ oldIndex: cx,
71302
+ newIndex: cy
71303
+ });
71304
+ }
71305
+ ops.reverse();
71306
+ return ops;
71307
+ }
71308
+ }
71309
+ }
71310
+ return [];
71311
+ }
71312
+ function computeWordDiff(oldLine, newLine) {
71313
+ const oldWords = tokenizeWords(oldLine);
71314
+ const newWords = tokenizeWords(newLine);
71315
+ const ops = myersDiff(oldWords, newWords);
71316
+ const oldSpans = [];
71317
+ const newSpans = [];
71318
+ for (const op of ops) {
71319
+ if (op.kind === "equal") {
71320
+ const text = op.oldItem ?? "";
71321
+ oldSpans.push({ text, kind: "equal" });
71322
+ newSpans.push({ text, kind: "equal" });
71323
+ } else if (op.kind === "delete") {
71324
+ oldSpans.push({ text: op.oldItem ?? "", kind: "deletion" });
71325
+ } else if (op.kind === "insert") {
71326
+ newSpans.push({ text: op.newItem ?? "", kind: "addition" });
71327
+ }
71328
+ }
71329
+ return { oldSpans, newSpans };
71330
+ }
71331
+ function buildUnifiedDiff(before, after, contextLines = 3) {
71332
+ const oldTokens = tokenizeLines(before);
71333
+ const newTokens = tokenizeLines(after);
71334
+ if (before === after) {
71335
+ return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
71336
+ }
71337
+ const ops = myersDiff(oldTokens, newTokens, (a, b) => a.text === b.text && a.ending === b.ending);
71338
+ const editIndices = ops.map((op, i) => op.kind !== "equal" ? i : -1).filter((i) => i !== -1);
71339
+ if (editIndices.length === 0) {
71340
+ return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
71341
+ }
71342
+ const hunkRanges = [];
71343
+ let currentRange = {
71344
+ startIdx: Math.max(0, editIndices[0] - contextLines),
71345
+ endIdx: Math.min(ops.length - 1, editIndices[0] + contextLines)
71346
+ };
71347
+ for (let i = 1;i < editIndices.length; i++) {
71348
+ const editIdx = editIndices[i];
71349
+ const opStart = Math.max(0, editIdx - contextLines);
71350
+ const opEnd = Math.min(ops.length - 1, editIdx + contextLines);
71351
+ if (opStart <= currentRange.endIdx) {
71352
+ currentRange.endIdx = Math.max(currentRange.endIdx, opEnd);
71353
+ } else {
71354
+ hunkRanges.push(currentRange);
71355
+ currentRange = { startIdx: opStart, endIdx: opEnd };
71356
+ }
71357
+ }
71358
+ hunkRanges.push(currentRange);
71359
+ const hunks = [];
71360
+ for (const range of hunkRanges) {
71361
+ const lines = [];
71362
+ let oldCount = 0;
71363
+ let newCount = 0;
71364
+ let oldStart = null;
71365
+ let newStart = null;
71366
+ for (let i = range.startIdx;i <= range.endIdx; i++) {
71367
+ const op = ops[i];
71368
+ if (op.kind === "equal") {
71369
+ const oIdx = op.oldIndex + 1;
71370
+ const nIdx = op.newIndex + 1;
71371
+ if (oldStart === null)
71372
+ oldStart = oIdx;
71373
+ if (newStart === null)
71374
+ newStart = nIdx;
71375
+ lines.push({
71376
+ kind: "context",
71377
+ text: op.oldItem.text,
71378
+ ending: op.oldItem.ending,
71379
+ oldLineNumber: oIdx,
71380
+ newLineNumber: nIdx
71381
+ });
71382
+ oldCount++;
71383
+ newCount++;
71384
+ } else if (op.kind === "delete") {
71385
+ const oIdx = op.oldIndex + 1;
71386
+ if (oldStart === null)
71387
+ oldStart = oIdx;
71388
+ lines.push({
71389
+ kind: "deletion",
71390
+ text: op.oldItem.text,
71391
+ ending: op.oldItem.ending,
71392
+ oldLineNumber: oIdx
71393
+ });
71394
+ oldCount++;
71395
+ } else if (op.kind === "insert") {
71396
+ const nIdx = op.newIndex + 1;
71397
+ if (newStart === null)
71398
+ newStart = nIdx;
71399
+ lines.push({
71400
+ kind: "addition",
71401
+ text: op.newItem.text,
71402
+ ending: op.newItem.ending,
71403
+ newLineNumber: nIdx
71404
+ });
71405
+ newCount++;
71406
+ }
71407
+ }
71408
+ let l = 0;
71409
+ while (l < lines.length) {
71410
+ if (lines[l]?.kind === "deletion") {
71411
+ const delStart = l;
71412
+ while (l < lines.length && lines[l]?.kind === "deletion")
71413
+ l++;
71414
+ const delEnd = l;
71415
+ const addStart = l;
71416
+ while (l < lines.length && lines[l]?.kind === "addition")
71417
+ l++;
71418
+ const addEnd = l;
71419
+ const delCount = delEnd - delStart;
71420
+ const addCount = addEnd - addStart;
71421
+ const pairCount = Math.min(delCount, addCount);
71422
+ for (let p = 0;p < pairCount; p++) {
71423
+ const delLine = lines[delStart + p];
71424
+ const addLine = lines[addStart + p];
71425
+ const { oldSpans, newSpans } = computeWordDiff(delLine.text, addLine.text);
71426
+ delLine.spans = oldSpans;
71427
+ addLine.spans = newSpans;
71428
+ }
71429
+ } else {
71430
+ l++;
71431
+ }
71432
+ }
71433
+ hunks.push({
71434
+ oldStart: oldStart ?? (oldTokens.length === 0 ? 0 : 1),
71435
+ oldCount,
71436
+ newStart: newStart ?? (newTokens.length === 0 ? 0 : 1),
71437
+ newCount,
71438
+ lines
71439
+ });
71440
+ }
71441
+ return {
71442
+ hunks,
71443
+ oldLineCount: oldTokens.length,
71444
+ newLineCount: newTokens.length
71445
+ };
71446
+ }
71447
+
71448
+ // src/tools/edit-file/index.ts
71236
71449
  var editFileInputSchema = object({
71237
71450
  file_path: string2().describe("The path of the file to edit (relative to current working directory or absolute inside trusted workspace)."),
71238
71451
  old_string: string2().describe("The exact string to find and replace in the file."),
@@ -71260,27 +71473,54 @@ var editFileTool = {
71260
71473
  const added = result?.addedLines ?? 0;
71261
71474
  const removed = result?.removedLines ?? 0;
71262
71475
  const summary = `Added ${added} line${added === 1 ? "" : "s"}, removed ${removed} line${removed === 1 ? "" : "s"}`;
71263
- const oldLines = args.old_string.split(/\r?\n/);
71264
- const newLines = args.new_string.split(/\r?\n/);
71265
71476
  if (added === 0 && removed === 0)
71266
71477
  return summary;
71267
- const cap = 30;
71268
- const padWidth = String(Math.max(oldLines.length, newLines.length)).length;
71478
+ const diff = buildUnifiedDiff(args.old_string, args.new_string, 2);
71269
71479
  const theme = getTheme();
71270
71480
  const deleteStyle = (str) => themeBgColor(theme.diffDeleteBG)(themeColor2(theme.diffDeleteFG)(str));
71271
71481
  const addStyle = (str) => themeBgColor(theme.diffAddBG)(themeColor2(theme.diffAddFG)(str));
71272
- const removedDetail = oldLines.slice(0, cap).map((l, i) => deleteStyle(`${String(i + 1).padStart(padWidth)} -${l}`)).join(`
71273
- `);
71274
- const addedDetail = newLines.slice(0, cap).map((l, i) => addStyle(`${String(i + 1).padStart(padWidth)} +${l}`)).join(`
71275
- `);
71276
- const overflowOld = oldLines.length > cap ? `
71277
- … (${oldLines.length - cap} more removed)` : "";
71278
- const overflowNew = newLines.length > cap ? `
71279
- … (${newLines.length - cap} more added)` : "";
71280
- const diffBlock = [removedDetail + overflowOld, addedDetail + overflowNew].filter(Boolean).join(`
71281
- `);
71282
- return `${summary}
71283
- ${diffBlock}`;
71482
+ const contextStyle = (str) => source_default.dim(str);
71483
+ let maxLineNum = 1;
71484
+ for (const hunk of diff.hunks) {
71485
+ for (const line of hunk.lines) {
71486
+ if (line.oldLineNumber)
71487
+ maxLineNum = Math.max(maxLineNum, line.oldLineNumber);
71488
+ if (line.newLineNumber)
71489
+ maxLineNum = Math.max(maxLineNum, line.newLineNumber);
71490
+ }
71491
+ }
71492
+ const padWidth = Math.max(1, String(maxLineNum).length);
71493
+ const diffLines = [];
71494
+ const cap = 30;
71495
+ let count = 0;
71496
+ let overflow = 0;
71497
+ for (const hunk of diff.hunks) {
71498
+ for (const line of hunk.lines) {
71499
+ if (count >= cap) {
71500
+ overflow++;
71501
+ continue;
71502
+ }
71503
+ count++;
71504
+ if (line.kind === "deletion") {
71505
+ const numStr = String(line.oldLineNumber ?? "").padStart(padWidth, " ");
71506
+ const body = line.spans ? "-" + line.spans.map((s) => s.kind === "deletion" ? source_default.bold(s.text) : s.text).join("") : `-${line.text}`;
71507
+ diffLines.push(deleteStyle(`${numStr} ${body}`));
71508
+ } else if (line.kind === "addition") {
71509
+ const numStr = String(line.newLineNumber ?? "").padStart(padWidth, " ");
71510
+ const body = line.spans ? "+" + line.spans.map((s) => s.kind === "addition" ? source_default.bold(s.text) : s.text).join("") : `+${line.text}`;
71511
+ diffLines.push(addStyle(`${numStr} ${body}`));
71512
+ } else {
71513
+ const numStr = String(line.newLineNumber ?? line.oldLineNumber ?? "").padStart(padWidth, " ");
71514
+ diffLines.push(contextStyle(`${numStr} ${line.text}`));
71515
+ }
71516
+ }
71517
+ }
71518
+ if (overflow > 0) {
71519
+ diffLines.push(source_default.dim(` … (${overflow} more lines)`));
71520
+ }
71521
+ return diffLines.length > 0 ? `${summary}
71522
+ ${diffLines.join(`
71523
+ `)}` : summary;
71284
71524
  },
71285
71525
  execute: async (args, context) => {
71286
71526
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
@@ -72295,15 +72535,23 @@ async function evaluateBashPermission(req, context) {
72295
72535
 
72296
72536
  // src/tools/bash/index.ts
72297
72537
  var bashInputSchema = object({
72298
- command: string2().min(1).describe("The command to execute in the system shell."),
72299
- explanation: string2().min(1).max(240).describe("Brief sentence explaining why this command is needed (displayed in permission prompt).")
72538
+ command: string2({
72539
+ required_error: "command is required"
72540
+ }).min(1, "command cannot be empty").describe("The command to execute in the system shell."),
72541
+ explanation: string2({
72542
+ required_error: "An explanation is mandatory for running a bash command.",
72543
+ invalid_type_error: "explanation must be a string."
72544
+ }).min(1, "An explanation is mandatory for running a bash command.").max(240).describe("Mandatory brief sentence explaining why this command is needed and what it does (displayed in permission prompt).")
72300
72545
  });
72301
72546
  var bashTool = {
72302
72547
  name: "bash",
72303
72548
  displayName: "Bash",
72304
- description: "Executes a command in the platform shell. Commands that exceed the foreground limit (~12s) automatically continue as background shell tasks. Use task_read, task_send_input, and task_kill to manage background tasks. Note: Bash commands are NOT tracked by the file checkpoint system (/rewind).",
72549
+ description: "Executes a command in the platform shell. An explanation of what the command does and why it is needed is MANDATORY. Commands that exceed the foreground limit (~12s) automatically continue as background shell tasks. Use task_read, task_send_input, and task_kill to manage background tasks. Note: Bash commands are NOT tracked by the file checkpoint system (/rewind).",
72305
72550
  parameters: bashInputSchema,
72306
72551
  confirmationPolicy: "never",
72552
+ summarizeArgs: (args) => {
72553
+ return args.command ?? "";
72554
+ },
72307
72555
  summarize: (_args, result) => {
72308
72556
  if (result && "status" in result && result.status === "backgrounded") {
72309
72557
  return `Moved to background · task id: ${result.taskId}`;
@@ -72312,10 +72560,13 @@ var bashTool = {
72312
72560
  return `Ran successfully · exit code: ${code}`;
72313
72561
  },
72314
72562
  execute: async (args, context) => {
72315
- const command = args.command.trim();
72316
- const explanation = args.explanation.trim();
72563
+ const command = typeof args?.command === "string" ? args.command.trim() : "";
72564
+ if (!command) {
72565
+ throw new Error("Command is required and cannot be empty.");
72566
+ }
72567
+ const explanation = typeof args?.explanation === "string" ? args.explanation.trim() : "";
72317
72568
  if (!explanation) {
72318
- throw new Error("An explanation is required for running a bash command.");
72569
+ throw new Error("An explanation is mandatory for running a bash command.");
72319
72570
  }
72320
72571
  const perm = await evaluateBashPermission({ command, explanation }, context);
72321
72572
  if (!perm.allowed) {
@@ -72440,7 +72691,7 @@ var taskKillTool = {
72440
72691
  // package.json
72441
72692
  var package_default = {
72442
72693
  name: "steward-cli",
72443
- version: "0.11.0",
72694
+ version: "0.13.0",
72444
72695
  description: "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
72445
72696
  author: "sapirrior",
72446
72697
  license: "MIT",
@@ -72657,6 +72908,727 @@ var webSearchTool = {
72657
72908
  };
72658
72909
  }
72659
72910
  };
72911
+ // src/services/todos/store.ts
72912
+ import {
72913
+ existsSync as existsSync12,
72914
+ mkdirSync as mkdirSync9,
72915
+ readFileSync as readFileSync12,
72916
+ renameSync as renameSync7,
72917
+ unlinkSync as unlinkSync7,
72918
+ writeFileSync as writeFileSync3,
72919
+ openSync as openSync8,
72920
+ fsyncSync as fsyncSync7,
72921
+ closeSync as closeSync8
72922
+ } from "node:fs";
72923
+ import { join as join13 } from "node:path";
72924
+ import { randomUUID as randomUUID7 } from "node:crypto";
72925
+
72926
+ // src/config/env.ts
72927
+ var ALL_PROVIDER_NAMES = [
72928
+ "gemini",
72929
+ "anthropic",
72930
+ "openai",
72931
+ "xai",
72932
+ "mistral",
72933
+ "deepseek",
72934
+ "openrouter",
72935
+ "custom"
72936
+ ];
72937
+ function getEnvConfig() {
72938
+ const geminiApiKey = process.env["GEMINI_API_KEY"]?.trim() || process.env["GOOGLE_GENERATIVE_AI_API_KEY"]?.trim() || undefined;
72939
+ const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || undefined;
72940
+ const openaiApiKey = process.env["OPENAI_API_KEY"]?.trim() || undefined;
72941
+ const xaiApiKey = process.env["XAI_API_KEY"]?.trim() || undefined;
72942
+ const mistralApiKey = process.env["MISTRAL_API_KEY"]?.trim() || undefined;
72943
+ const deepseekApiKey = process.env["DEEPSEEK_API_KEY"]?.trim() || undefined;
72944
+ const openrouterApiKey = process.env["OPENROUTER_API_KEY"]?.trim() || undefined;
72945
+ const customApiKey = process.env["CUSTOM_API_KEY"]?.trim() || undefined;
72946
+ const customApiModelName = process.env["CUSTOM_API_MODEL_NAME"]?.trim() || undefined;
72947
+ const customApiUrl = process.env["CUSTOM_API_URL"]?.trim() || undefined;
72948
+ return {
72949
+ geminiApiKey,
72950
+ anthropicApiKey,
72951
+ openaiApiKey,
72952
+ xaiApiKey,
72953
+ mistralApiKey,
72954
+ deepseekApiKey,
72955
+ openrouterApiKey,
72956
+ custom: {
72957
+ apiKey: customApiKey,
72958
+ modelName: customApiModelName,
72959
+ baseURL: customApiUrl
72960
+ }
72961
+ };
72962
+ }
72963
+ function hasProviderConfig(provider, config = getEnvConfig()) {
72964
+ switch (provider) {
72965
+ case "gemini":
72966
+ return Boolean(config.geminiApiKey);
72967
+ case "anthropic":
72968
+ return Boolean(config.anthropicApiKey);
72969
+ case "openai":
72970
+ return Boolean(config.openaiApiKey);
72971
+ case "xai":
72972
+ return Boolean(config.xaiApiKey);
72973
+ case "mistral":
72974
+ return Boolean(config.mistralApiKey);
72975
+ case "deepseek":
72976
+ return Boolean(config.deepseekApiKey);
72977
+ case "openrouter":
72978
+ return Boolean(config.openrouterApiKey);
72979
+ case "custom":
72980
+ return Boolean(config.custom.baseURL && config.custom.modelName);
72981
+ }
72982
+ }
72983
+ function getAvailableProviders(config = getEnvConfig()) {
72984
+ const providers = [];
72985
+ if (hasProviderConfig("gemini", config))
72986
+ providers.push("gemini");
72987
+ if (hasProviderConfig("anthropic", config))
72988
+ providers.push("anthropic");
72989
+ if (hasProviderConfig("openai", config))
72990
+ providers.push("openai");
72991
+ if (hasProviderConfig("xai", config))
72992
+ providers.push("xai");
72993
+ if (hasProviderConfig("mistral", config))
72994
+ providers.push("mistral");
72995
+ if (hasProviderConfig("deepseek", config))
72996
+ providers.push("deepseek");
72997
+ if (hasProviderConfig("openrouter", config))
72998
+ providers.push("openrouter");
72999
+ if (hasProviderConfig("custom", config))
73000
+ providers.push("custom");
73001
+ return providers;
73002
+ }
73003
+ // src/services/todos/store.ts
73004
+ var VALID_STATUSES = [
73005
+ "pending",
73006
+ "in_progress",
73007
+ "completed",
73008
+ "cancelled",
73009
+ "blocked"
73010
+ ];
73011
+ var MIN_ITEMS = 2;
73012
+ var MAX_ITEMS = 10;
73013
+
73014
+ class SessionLockQueue {
73015
+ queues = new Map;
73016
+ async runExclusive(sessionId, task) {
73017
+ const prev = this.queues.get(sessionId) ?? Promise.resolve();
73018
+ let release;
73019
+ const current = new Promise((resolve) => {
73020
+ release = resolve;
73021
+ });
73022
+ this.queues.set(sessionId, prev.then(() => current, () => current));
73023
+ try {
73024
+ await prev;
73025
+ return await task();
73026
+ } finally {
73027
+ release();
73028
+ if (this.queues.get(sessionId) === current) {
73029
+ this.queues.delete(sessionId);
73030
+ }
73031
+ }
73032
+ }
73033
+ }
73034
+ var sessionLocks = new SessionLockQueue;
73035
+ function sanitizeSessionId(sessionId) {
73036
+ if (!sessionId || typeof sessionId !== "string") {
73037
+ throw new Error("Invalid sessionId: must be a non-empty string");
73038
+ }
73039
+ const clean = sessionId.trim();
73040
+ if (!clean || clean.includes("..") || clean.includes("/") || clean.includes("\\")) {
73041
+ throw new Error(`Invalid sessionId for path resolution: "${sessionId}"`);
73042
+ }
73043
+ return clean;
73044
+ }
73045
+ function getSessionTodosDir(sessionId) {
73046
+ const safeId = sanitizeSessionId(sessionId);
73047
+ return join13(getSettingsDir(), "todos", safeId);
73048
+ }
73049
+ function getSessionTodosFilePath(sessionId) {
73050
+ return join13(getSessionTodosDir(sessionId), "todos.json");
73051
+ }
73052
+ function validateTodoList(todos) {
73053
+ if (!Array.isArray(todos)) {
73054
+ throw new Error("Invalid todos format: expected an array of todo items");
73055
+ }
73056
+ if (todos.length < MIN_ITEMS || todos.length > MAX_ITEMS) {
73057
+ throw new Error(`Todo list must contain between ${MIN_ITEMS} and ${MAX_ITEMS} items (received ${todos.length})`);
73058
+ }
73059
+ const seenIds = new Set;
73060
+ let inProgressCount = 0;
73061
+ const validated = [];
73062
+ for (let i = 0;i < todos.length; i++) {
73063
+ const item = todos[i];
73064
+ if (!item || typeof item !== "object") {
73065
+ throw new Error(`Invalid todo at index ${i}: item must be an object`);
73066
+ }
73067
+ const { id, description, status } = item;
73068
+ if (typeof id !== "string" || !id.trim()) {
73069
+ throw new Error(`Invalid todo at index ${i}: id must be a non-empty string`);
73070
+ }
73071
+ const trimmedId = id.trim();
73072
+ if (seenIds.has(trimmedId)) {
73073
+ throw new Error(`Duplicate todo id "${trimmedId}" at index ${i}`);
73074
+ }
73075
+ seenIds.add(trimmedId);
73076
+ if (typeof description !== "string" || !description.trim()) {
73077
+ throw new Error(`Invalid todo at index ${i} (${trimmedId}): description must be a non-empty string`);
73078
+ }
73079
+ if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
73080
+ throw new Error(`Invalid todo at index ${i} (${trimmedId}): status must be one of ${VALID_STATUSES.join(", ")} (got "${status}")`);
73081
+ }
73082
+ if (status === "in_progress") {
73083
+ inProgressCount++;
73084
+ }
73085
+ validated.push({
73086
+ id: trimmedId,
73087
+ description: description.trim(),
73088
+ status
73089
+ });
73090
+ }
73091
+ if (inProgressCount > 1) {
73092
+ throw new Error(`At most 1 todo item can be 'in_progress' at any time (found ${inProgressCount} in_progress items)`);
73093
+ }
73094
+ return validated;
73095
+ }
73096
+ async function readTodoList(sessionId) {
73097
+ const filePath = getSessionTodosFilePath(sessionId);
73098
+ if (!existsSync12(filePath)) {
73099
+ return null;
73100
+ }
73101
+ const raw = readFileSync12(filePath, "utf-8");
73102
+ let parsed;
73103
+ try {
73104
+ parsed = JSON.parse(raw);
73105
+ } catch (err) {
73106
+ throw new Error(`Corrupted todo state for session "${sessionId}": failed to parse JSON (${err instanceof Error ? err.message : String(err)})`);
73107
+ }
73108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
73109
+ throw new Error(`Corrupted todo state for session "${sessionId}": root must be an object`);
73110
+ }
73111
+ const record = parsed;
73112
+ if (record.schemaVersion !== 1) {
73113
+ throw new Error(`Unsupported todo schemaVersion "${record.schemaVersion}" (expected 1) for session "${sessionId}"`);
73114
+ }
73115
+ if (typeof record.sessionId !== "string") {
73116
+ throw new Error(`Invalid todo state for session "${sessionId}": missing or invalid sessionId`);
73117
+ }
73118
+ const validatedTodos = validateTodoList(record.todos);
73119
+ return {
73120
+ schemaVersion: 1,
73121
+ sessionId: record.sessionId,
73122
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString(),
73123
+ todos: validatedTodos
73124
+ };
73125
+ }
73126
+ async function writeTodoList(sessionId, todos) {
73127
+ return sessionLocks.runExclusive(sessionId, async () => {
73128
+ const validated = validateTodoList(todos);
73129
+ const safeSessionId = sanitizeSessionId(sessionId);
73130
+ const dir = getSessionTodosDir(safeSessionId);
73131
+ if (!existsSync12(dir)) {
73132
+ mkdirSync9(dir, { recursive: true, mode: 448 });
73133
+ }
73134
+ const payload = {
73135
+ schemaVersion: 1,
73136
+ sessionId: safeSessionId,
73137
+ updatedAt: new Date().toISOString(),
73138
+ todos: validated
73139
+ };
73140
+ const targetPath = getSessionTodosFilePath(safeSessionId);
73141
+ const tempPath = join13(dir, `.todos.${randomUUID7()}.tmp`);
73142
+ const jsonStr = JSON.stringify(payload, null, 2) + `
73143
+ `;
73144
+ try {
73145
+ writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73146
+ const fd = openSync8(tempPath, "r");
73147
+ try {
73148
+ fsyncSync7(fd);
73149
+ } finally {
73150
+ closeSync8(fd);
73151
+ }
73152
+ renameSync7(tempPath, targetPath);
73153
+ } catch (err) {
73154
+ try {
73155
+ if (existsSync12(tempPath)) {
73156
+ unlinkSync7(tempPath);
73157
+ }
73158
+ } catch {}
73159
+ throw err;
73160
+ }
73161
+ return payload;
73162
+ });
73163
+ }
73164
+ async function updateTodoItem(sessionId, todoId, updates) {
73165
+ return sessionLocks.runExclusive(sessionId, async () => {
73166
+ if (!todoId || typeof todoId !== "string" || !todoId.trim()) {
73167
+ throw new Error("todoId must be a non-empty string");
73168
+ }
73169
+ const targetId = todoId.trim();
73170
+ const current = await readTodoList(sessionId);
73171
+ if (!current) {
73172
+ throw new Error(`No todo list found for session "${sessionId}". Create one first with TodoWrite.`);
73173
+ }
73174
+ const itemIndex = current.todos.findIndex((t) => t.id === targetId);
73175
+ if (itemIndex === -1) {
73176
+ throw new Error(`Todo item with id "${targetId}" not found in current session todo list`);
73177
+ }
73178
+ if (updates.status === undefined && updates.description === undefined) {
73179
+ throw new Error("At least one of status or description must be provided for update");
73180
+ }
73181
+ const currentItem = current.todos[itemIndex];
73182
+ const newStatus = updates.status !== undefined ? updates.status : currentItem.status;
73183
+ const newDescription = updates.description !== undefined ? updates.description.trim() : currentItem.description;
73184
+ if (!newDescription) {
73185
+ throw new Error("Updated description cannot be empty");
73186
+ }
73187
+ const updatedTodos = current.todos.map((item, idx) => {
73188
+ if (idx !== itemIndex) {
73189
+ return item;
73190
+ }
73191
+ return {
73192
+ ...item,
73193
+ status: newStatus,
73194
+ description: newDescription
73195
+ };
73196
+ });
73197
+ const validated = validateTodoList(updatedTodos);
73198
+ const safeSessionId = sanitizeSessionId(sessionId);
73199
+ const dir = getSessionTodosDir(safeSessionId);
73200
+ if (!existsSync12(dir)) {
73201
+ mkdirSync9(dir, { recursive: true, mode: 448 });
73202
+ }
73203
+ const payload = {
73204
+ schemaVersion: 1,
73205
+ sessionId: safeSessionId,
73206
+ updatedAt: new Date().toISOString(),
73207
+ todos: validated
73208
+ };
73209
+ const targetPath = getSessionTodosFilePath(safeSessionId);
73210
+ const tempPath = join13(dir, `.todos.${randomUUID7()}.tmp`);
73211
+ const jsonStr = JSON.stringify(payload, null, 2) + `
73212
+ `;
73213
+ try {
73214
+ writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73215
+ const fd = openSync8(tempPath, "r");
73216
+ try {
73217
+ fsyncSync7(fd);
73218
+ } finally {
73219
+ closeSync8(fd);
73220
+ }
73221
+ renameSync7(tempPath, targetPath);
73222
+ } catch (err) {
73223
+ try {
73224
+ if (existsSync12(tempPath)) {
73225
+ unlinkSync7(tempPath);
73226
+ }
73227
+ } catch {}
73228
+ throw err;
73229
+ }
73230
+ return payload;
73231
+ });
73232
+ }
73233
+ // src/tools/todo-write/index.ts
73234
+ var todoItemSchema = object({
73235
+ id: string2().min(1, "id is required").describe("Unique identifier for the todo item"),
73236
+ description: string2().min(1, "description is required").describe("Clear description of the task"),
73237
+ status: _enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).describe("Current status of the todo item")
73238
+ });
73239
+ var todoWriteParamsSchema = object({
73240
+ todos: array(todoItemSchema).min(2, "Todo list must contain at least 2 items").max(10, "Todo list must contain at most 10 items").describe("Complete list of 2 to 10 todo items establishing the execution plan")
73241
+ });
73242
+ function formatTodoCheckbox(status) {
73243
+ switch (status) {
73244
+ case "completed":
73245
+ return "[x]";
73246
+ case "in_progress":
73247
+ return `[${figures.sliderPointer}]`;
73248
+ case "cancelled":
73249
+ return `[${figures.cross}]`;
73250
+ case "blocked":
73251
+ return `[${figures.warning}]`;
73252
+ case "pending":
73253
+ default:
73254
+ return "[ ]";
73255
+ }
73256
+ }
73257
+ function formatTodoListSummary(todos, maxItems = 10) {
73258
+ const visible = todos.slice(0, maxItems);
73259
+ const hiddenCount = todos.length - visible.length;
73260
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox(t.status)} ${t.description}`);
73261
+ if (hiddenCount > 0) {
73262
+ lines.push(`… and ${hiddenCount} more`);
73263
+ }
73264
+ return lines.join(`
73265
+ `);
73266
+ }
73267
+ var todoWriteTool = {
73268
+ name: "TodoWrite",
73269
+ displayName: "TodoWrite",
73270
+ icon: "\uD83D\uDCDD",
73271
+ description: "Initializes or replaces the current session todo list with 2 to 10 items. Use this to establish or replace the multi-step execution plan for the session.",
73272
+ parameters: todoWriteParamsSchema,
73273
+ summarizeArgs(args) {
73274
+ if (!args.todos || args.todos.length === 0)
73275
+ return "";
73276
+ if (args.todos.length <= 2) {
73277
+ return args.todos.map((t) => `"${t.description}"`).join(", ");
73278
+ }
73279
+ return `"${args.todos[0]?.description}", "${args.todos[1]?.description}", +${args.todos.length - 2} more`;
73280
+ },
73281
+ async execute(args, context) {
73282
+ if (!context.sessionId) {
73283
+ throw new Error("TodoWrite requires an active session ID in ToolContext");
73284
+ }
73285
+ return writeTodoList(context.sessionId, args.todos);
73286
+ },
73287
+ summarize(args, result) {
73288
+ const list = result?.todos ?? args.todos;
73289
+ const total = list.length;
73290
+ const header = `Added ${total} todos`;
73291
+ const details = formatTodoListSummary(list);
73292
+ return `${header}
73293
+ ${details}`;
73294
+ }
73295
+ };
73296
+
73297
+ // src/tools/todo-update/index.ts
73298
+ var todoUpdateParamsSchema = object({
73299
+ id: string2().min(1, "id is required").describe("ID of the existing todo item to update"),
73300
+ status: _enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).optional().describe("New status for the todo item"),
73301
+ description: string2().min(1).optional().describe("Updated description for the todo item")
73302
+ });
73303
+ function formatTodoCheckbox2(status) {
73304
+ switch (status) {
73305
+ case "completed":
73306
+ return "[x]";
73307
+ case "in_progress":
73308
+ return `[${figures.sliderPointer}]`;
73309
+ case "cancelled":
73310
+ return `[${figures.cross}]`;
73311
+ case "blocked":
73312
+ return `[${figures.warning}]`;
73313
+ case "pending":
73314
+ default:
73315
+ return "[ ]";
73316
+ }
73317
+ }
73318
+ function formatTodoListSummary2(todos, maxItems = 10) {
73319
+ const visible = todos.slice(0, maxItems);
73320
+ const hiddenCount = todos.length - visible.length;
73321
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox2(t.status)} ${t.description}`);
73322
+ if (hiddenCount > 0) {
73323
+ lines.push(`… and ${hiddenCount} more`);
73324
+ }
73325
+ return lines.join(`
73326
+ `);
73327
+ }
73328
+ var todoUpdateTool = {
73329
+ name: "TodoUpdate",
73330
+ displayName: "TodoUpdate",
73331
+ icon: "✓",
73332
+ description: "Updates a single todo item status or description in the current session. Cannot create new items or update multiple items at once.",
73333
+ parameters: todoUpdateParamsSchema,
73334
+ summarizeArgs(args) {
73335
+ const parts = [`id: "${args.id}"`];
73336
+ if (args.status)
73337
+ parts.push(`status: "${args.status}"`);
73338
+ if (args.description)
73339
+ parts.push(`desc: "${args.description}"`);
73340
+ return parts.join(", ");
73341
+ },
73342
+ async execute(args, context) {
73343
+ if (!context.sessionId) {
73344
+ throw new Error("TodoUpdate requires an active session ID in ToolContext");
73345
+ }
73346
+ return updateTodoItem(context.sessionId, args.id, {
73347
+ status: args.status,
73348
+ description: args.description
73349
+ });
73350
+ },
73351
+ summarize(args, result) {
73352
+ if (!result || !result.todos) {
73353
+ return `Todo ${args.id} updated`;
73354
+ }
73355
+ const total = result.todos.length;
73356
+ const completed = result.todos.filter((t) => t.status === "completed").length;
73357
+ const updatedItem = result.todos.find((t) => t.id === args.id);
73358
+ const statusText = updatedItem?.status ?? args.status ?? "updated";
73359
+ const header = `Todo ${args.id} ${statusText} · ${completed}/${total} done`;
73360
+ const details = formatTodoListSummary2(result.todos);
73361
+ return `${header}
73362
+ ${details}`;
73363
+ }
73364
+ };
73365
+
73366
+ // src/tools/todo-read/index.ts
73367
+ var todoReadParamsSchema = object({});
73368
+ function formatTodoCheckbox3(status) {
73369
+ switch (status) {
73370
+ case "completed":
73371
+ return "[x]";
73372
+ case "in_progress":
73373
+ return `[${figures.sliderPointer}]`;
73374
+ case "cancelled":
73375
+ return `[${figures.cross}]`;
73376
+ case "blocked":
73377
+ return `[${figures.warning}]`;
73378
+ case "pending":
73379
+ default:
73380
+ return "[ ]";
73381
+ }
73382
+ }
73383
+ function formatTodoListSummary3(todos, maxItems = 10) {
73384
+ const visible = todos.slice(0, maxItems);
73385
+ const hiddenCount = todos.length - visible.length;
73386
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox3(t.status)} ${t.description}`);
73387
+ if (hiddenCount > 0) {
73388
+ lines.push(`… and ${hiddenCount} more`);
73389
+ }
73390
+ return lines.join(`
73391
+ `);
73392
+ }
73393
+ var todoReadTool = {
73394
+ name: "TodoRead",
73395
+ displayName: "TodoRead",
73396
+ icon: "\uD83D\uDCCB",
73397
+ description: "Reads the current session todo list without modifying it. Returns an empty result if no todos have been established yet.",
73398
+ parameters: todoReadParamsSchema,
73399
+ summarizeArgs() {
73400
+ return "";
73401
+ },
73402
+ async execute(_args, context) {
73403
+ if (!context.sessionId) {
73404
+ throw new Error("TodoRead requires an active session ID in ToolContext");
73405
+ }
73406
+ const list = await readTodoList(context.sessionId);
73407
+ if (!list) {
73408
+ return {
73409
+ hasTodos: false,
73410
+ todos: [],
73411
+ message: "No todos currently established for this session."
73412
+ };
73413
+ }
73414
+ return {
73415
+ hasTodos: true,
73416
+ todos: list.todos,
73417
+ updatedAt: list.updatedAt
73418
+ };
73419
+ },
73420
+ summarize(_args, result) {
73421
+ if (!result || !result.hasTodos || result.todos.length === 0) {
73422
+ return "No active todos";
73423
+ }
73424
+ const total = result.todos.length;
73425
+ const completed = result.todos.filter((t) => t.status === "completed").length;
73426
+ const active = result.todos.find((t) => t.status === "in_progress");
73427
+ const header = active ? `Todos ${completed}/${total} done · ▶ ${active.description}` : `Todos ${completed}/${total} done`;
73428
+ const details = formatTodoListSummary3(result.todos);
73429
+ return `${header}
73430
+ ${details}`;
73431
+ }
73432
+ };
73433
+ // src/skills/discovery.ts
73434
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6 } from "node:fs";
73435
+ import { homedir as homedir5 } from "node:os";
73436
+ import { isAbsolute as isAbsolute6, join as join14, normalize, resolve as resolve9, sep as sep3 } from "node:path";
73437
+ var MAX_SKILL_FILE_SIZE = 64 * 1024;
73438
+ function parseSkillFrontmatter(content) {
73439
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
73440
+ if (!match || !match[1]) {
73441
+ return null;
73442
+ }
73443
+ const rawYaml = match[1];
73444
+ const lines = rawYaml.split(/\r?\n/);
73445
+ const result = {};
73446
+ let currentKey = null;
73447
+ let descriptionLines = [];
73448
+ for (const line of lines) {
73449
+ const trimmed = line.trim();
73450
+ if (!trimmed || trimmed.startsWith("#"))
73451
+ continue;
73452
+ const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
73453
+ if (keyMatch && keyMatch[1]) {
73454
+ const key = keyMatch[1].toLowerCase();
73455
+ const val = (keyMatch[2] ?? "").trim();
73456
+ if (key === "name") {
73457
+ currentKey = "name";
73458
+ result.name = unquote(val);
73459
+ } else if (key === "description") {
73460
+ currentKey = "description";
73461
+ if (val === ">" || val === ">-" || val === "|" || val === "|-") {
73462
+ descriptionLines = [];
73463
+ } else if (val) {
73464
+ descriptionLines = [unquote(val)];
73465
+ } else {
73466
+ descriptionLines = [];
73467
+ }
73468
+ } else {
73469
+ currentKey = null;
73470
+ }
73471
+ } else if (currentKey === "description" && (line.startsWith(" ") || line.startsWith("\t"))) {
73472
+ descriptionLines.push(trimmed);
73473
+ }
73474
+ }
73475
+ if (descriptionLines.length > 0) {
73476
+ result.description = descriptionLines.join(" ").trim();
73477
+ }
73478
+ return result;
73479
+ }
73480
+ function unquote(str) {
73481
+ const trimmed = str.trim();
73482
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
73483
+ return trimmed.slice(1, -1);
73484
+ }
73485
+ return trimmed;
73486
+ }
73487
+ function discoverSkills(cwd = process.cwd()) {
73488
+ const targets = [
73489
+ { dir: join14(cwd, ".agents", "skills"), source: "workspace" },
73490
+ { dir: join14(homedir5(), ".agents", "skills"), source: "user-agents" },
73491
+ { dir: join14(homedir5(), ".steward", "skills"), source: "user-steward" }
73492
+ ];
73493
+ const skillMap = new Map;
73494
+ for (const target of targets) {
73495
+ if (!existsSync13(target.dir)) {
73496
+ continue;
73497
+ }
73498
+ try {
73499
+ const entries = readdirSync5(target.dir, { withFileTypes: true });
73500
+ for (const entry of entries) {
73501
+ if (!entry.isDirectory())
73502
+ continue;
73503
+ const skillDir = join14(target.dir, entry.name);
73504
+ const skillMdPath = join14(skillDir, "SKILL.md");
73505
+ const fallbackMdPath = join14(skillDir, "skill.md");
73506
+ const resolvedFile = existsSync13(skillMdPath) ? skillMdPath : existsSync13(fallbackMdPath) ? fallbackMdPath : null;
73507
+ if (!resolvedFile)
73508
+ continue;
73509
+ try {
73510
+ const raw = readFileSync13(resolvedFile, "utf-8");
73511
+ const frontmatter = parseSkillFrontmatter(raw);
73512
+ const skillName = frontmatter?.name || entry.name;
73513
+ const description = frontmatter?.description || "No description provided.";
73514
+ if (!skillMap.has(skillName)) {
73515
+ skillMap.set(skillName, {
73516
+ name: skillName,
73517
+ description,
73518
+ filePath: resolvedFile,
73519
+ dirPath: skillDir,
73520
+ source: target.source
73521
+ });
73522
+ }
73523
+ } catch {}
73524
+ }
73525
+ } catch {}
73526
+ }
73527
+ return Array.from(skillMap.values());
73528
+ }
73529
+ function getSkill(name, cwd = process.cwd()) {
73530
+ const cleanName = name.trim();
73531
+ const all = discoverSkills(cwd);
73532
+ return all.find((s) => s.name.toLowerCase() === cleanName.toLowerCase()) ?? null;
73533
+ }
73534
+ function readSkillResource(name, relativePath, cwd = process.cwd(), maxBytes = MAX_SKILL_FILE_SIZE) {
73535
+ const skill = getSkill(name, cwd);
73536
+ if (!skill) {
73537
+ throw new Error(`Skill "${name}" not found`);
73538
+ }
73539
+ let targetFilePath;
73540
+ if (!relativePath || !relativePath.trim()) {
73541
+ targetFilePath = skill.filePath;
73542
+ } else {
73543
+ const trimmedPath = relativePath.trim();
73544
+ if (isAbsolute6(trimmedPath)) {
73545
+ throw new Error(`Invalid skill resource path: absolute paths are not permitted (${trimmedPath})`);
73546
+ }
73547
+ const normalizedRelative = normalize(trimmedPath);
73548
+ if (normalizedRelative.startsWith("..") || normalizedRelative.includes(`${sep3}..`)) {
73549
+ throw new Error(`Invalid skill resource path: directory traversal ("..") is forbidden (${trimmedPath})`);
73550
+ }
73551
+ targetFilePath = resolve9(skill.dirPath, normalizedRelative);
73552
+ const resolvedDir = resolve9(skill.dirPath);
73553
+ if (!targetFilePath.startsWith(resolvedDir + sep3) && targetFilePath !== resolvedDir) {
73554
+ throw new Error(`Resource "${trimmedPath}" resolves outside skill directory`);
73555
+ }
73556
+ }
73557
+ if (!existsSync13(targetFilePath)) {
73558
+ throw new Error(`Skill resource file "${targetFilePath}" does not exist`);
73559
+ }
73560
+ const stat = statSync6(targetFilePath);
73561
+ if (!stat.isFile()) {
73562
+ throw new Error(`Skill resource path "${targetFilePath}" is not a regular file`);
73563
+ }
73564
+ if (stat.size > maxBytes) {
73565
+ throw new Error(`Skill resource file exceeds size limit of ${maxBytes} bytes (file is ${stat.size} bytes)`);
73566
+ }
73567
+ const content = readFileSync13(targetFilePath, "utf-8");
73568
+ return {
73569
+ name: skill.name,
73570
+ description: skill.description,
73571
+ source: skill.source,
73572
+ resourcePath: targetFilePath,
73573
+ content
73574
+ };
73575
+ }
73576
+ // src/tools/skill-list/index.ts
73577
+ var skillListParamsSchema = object({});
73578
+ var skillListTool = {
73579
+ name: "SkillList",
73580
+ displayName: "SkillList",
73581
+ icon: "\uD83D\uDCA1",
73582
+ description: "Discovers and lists all available specialized skills and their descriptions. Use this when you need specialized domain knowledge or workflows.",
73583
+ parameters: skillListParamsSchema,
73584
+ summarizeArgs() {
73585
+ return "";
73586
+ },
73587
+ async execute(_args, context) {
73588
+ const cwd = context?.cwd ?? process.cwd();
73589
+ const skills = discoverSkills(cwd);
73590
+ return skills.map((s) => ({
73591
+ name: s.name,
73592
+ description: s.description,
73593
+ source: s.source
73594
+ })).sort((a, b) => a.name.localeCompare(b.name));
73595
+ },
73596
+ summarize(_args, result) {
73597
+ const count = result?.length ?? 0;
73598
+ if (count === 0) {
73599
+ return "Discovered 0 skills";
73600
+ }
73601
+ return `Discovered ${count} skill${count === 1 ? "" : "s"}`;
73602
+ }
73603
+ };
73604
+
73605
+ // src/tools/skill-read/index.ts
73606
+ var skillReadParamsSchema = object({
73607
+ name: string2().min(1, "Skill name is required"),
73608
+ path: string2().optional()
73609
+ });
73610
+ var skillReadTool = {
73611
+ name: "SkillRead",
73612
+ displayName: "SkillRead",
73613
+ icon: "\uD83D\uDCD6",
73614
+ description: "Loads and reads the instructions (SKILL.md) or a relative resource file of a specialized skill by name. Use this when a task requires specialized guidelines.",
73615
+ parameters: skillReadParamsSchema,
73616
+ summarizeArgs(args) {
73617
+ if (args.path) {
73618
+ return `name="${args.name}" path="${args.path}"`;
73619
+ }
73620
+ return `name="${args.name}"`;
73621
+ },
73622
+ async execute(args, context) {
73623
+ const cwd = context?.cwd ?? process.cwd();
73624
+ return readSkillResource(args.name, args.path, cwd);
73625
+ },
73626
+ summarize(args, result) {
73627
+ const target = args.path ? `${args.name}/${args.path}` : args.name;
73628
+ const len = result?.content ? ` (${result.content.length} bytes)` : "";
73629
+ return `SkillRead: ${target}${len}`;
73630
+ }
73631
+ };
72660
73632
 
72661
73633
  // src/tools/index.ts
72662
73634
  var builtInTools = [
@@ -72672,7 +73644,12 @@ var builtInTools = [
72672
73644
  taskSendInputTool,
72673
73645
  taskKillTool,
72674
73646
  webFetchTool,
72675
- webSearchTool
73647
+ webSearchTool,
73648
+ todoWriteTool,
73649
+ todoUpdateTool,
73650
+ todoReadTool,
73651
+ skillListTool,
73652
+ skillReadTool
72676
73653
  ];
72677
73654
  for (const tool of builtInTools) {
72678
73655
  defaultToolCatalog.register(tool);
@@ -72879,84 +73856,6 @@ class ShellTaskManager {
72879
73856
  }
72880
73857
  }
72881
73858
  }
72882
-
72883
- // src/config/env.ts
72884
- var ALL_PROVIDER_NAMES = [
72885
- "gemini",
72886
- "anthropic",
72887
- "openai",
72888
- "xai",
72889
- "mistral",
72890
- "deepseek",
72891
- "openrouter",
72892
- "custom"
72893
- ];
72894
- function getEnvConfig() {
72895
- const geminiApiKey = process.env["GEMINI_API_KEY"]?.trim() || process.env["GOOGLE_GENERATIVE_AI_API_KEY"]?.trim() || undefined;
72896
- const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || undefined;
72897
- const openaiApiKey = process.env["OPENAI_API_KEY"]?.trim() || undefined;
72898
- const xaiApiKey = process.env["XAI_API_KEY"]?.trim() || undefined;
72899
- const mistralApiKey = process.env["MISTRAL_API_KEY"]?.trim() || undefined;
72900
- const deepseekApiKey = process.env["DEEPSEEK_API_KEY"]?.trim() || undefined;
72901
- const openrouterApiKey = process.env["OPENROUTER_API_KEY"]?.trim() || undefined;
72902
- const customApiKey = process.env["CUSTOM_API_KEY"]?.trim() || undefined;
72903
- const customApiModelName = process.env["CUSTOM_API_MODEL_NAME"]?.trim() || undefined;
72904
- const customApiUrl = process.env["CUSTOM_API_URL"]?.trim() || undefined;
72905
- return {
72906
- geminiApiKey,
72907
- anthropicApiKey,
72908
- openaiApiKey,
72909
- xaiApiKey,
72910
- mistralApiKey,
72911
- deepseekApiKey,
72912
- openrouterApiKey,
72913
- custom: {
72914
- apiKey: customApiKey,
72915
- modelName: customApiModelName,
72916
- baseURL: customApiUrl
72917
- }
72918
- };
72919
- }
72920
- function hasProviderConfig(provider, config = getEnvConfig()) {
72921
- switch (provider) {
72922
- case "gemini":
72923
- return Boolean(config.geminiApiKey);
72924
- case "anthropic":
72925
- return Boolean(config.anthropicApiKey);
72926
- case "openai":
72927
- return Boolean(config.openaiApiKey);
72928
- case "xai":
72929
- return Boolean(config.xaiApiKey);
72930
- case "mistral":
72931
- return Boolean(config.mistralApiKey);
72932
- case "deepseek":
72933
- return Boolean(config.deepseekApiKey);
72934
- case "openrouter":
72935
- return Boolean(config.openrouterApiKey);
72936
- case "custom":
72937
- return Boolean(config.custom.baseURL && config.custom.modelName);
72938
- }
72939
- }
72940
- function getAvailableProviders(config = getEnvConfig()) {
72941
- const providers = [];
72942
- if (hasProviderConfig("gemini", config))
72943
- providers.push("gemini");
72944
- if (hasProviderConfig("anthropic", config))
72945
- providers.push("anthropic");
72946
- if (hasProviderConfig("openai", config))
72947
- providers.push("openai");
72948
- if (hasProviderConfig("xai", config))
72949
- providers.push("xai");
72950
- if (hasProviderConfig("mistral", config))
72951
- providers.push("mistral");
72952
- if (hasProviderConfig("deepseek", config))
72953
- providers.push("deepseek");
72954
- if (hasProviderConfig("openrouter", config))
72955
- providers.push("openrouter");
72956
- if (hasProviderConfig("custom", config))
72957
- providers.push("custom");
72958
- return providers;
72959
- }
72960
73859
  // src/errors/classifier.ts
72961
73860
  function classifyError(error) {
72962
73861
  if (!error) {
@@ -73101,15 +74000,15 @@ function classifyError(error) {
73101
74000
  };
73102
74001
  }
73103
74002
  // src/errors/logger.ts
73104
- import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync3 } from "node:fs";
73105
- import { homedir as homedir5 } from "node:os";
73106
- import { join as join13 } from "node:path";
74003
+ import { appendFileSync as appendFileSync3, existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync4 } from "node:fs";
74004
+ import { homedir as homedir6 } from "node:os";
74005
+ import { join as join15 } from "node:path";
73107
74006
  var SENSITIVE_KEY_REGEX = /api[_-]?key|token|secret|password|auth|credential|bearer/i;
73108
74007
  function getLogsRootDir() {
73109
74008
  if (process.env.STEWARD_LOGS_DIR) {
73110
74009
  return process.env.STEWARD_LOGS_DIR;
73111
74010
  }
73112
- return join13(homedir5(), ".steward", "logs");
74011
+ return join15(homedir6(), ".steward", "logs");
73113
74012
  }
73114
74013
  function getLogDateTime(d = new Date) {
73115
74014
  const year = d.getFullYear();
@@ -73193,11 +74092,11 @@ function logError(error, contextInfo) {
73193
74092
  try {
73194
74093
  const rootDir = getLogsRootDir();
73195
74094
  const { date, time } = getLogDateTime();
73196
- const dateDir = join13(rootDir, date);
73197
- if (!existsSync12(dateDir)) {
73198
- mkdirSync9(dateDir, { recursive: true });
74095
+ const dateDir = join15(rootDir, date);
74096
+ if (!existsSync14(dateDir)) {
74097
+ mkdirSync10(dateDir, { recursive: true });
73199
74098
  }
73200
- const logPath = join13(dateDir, `${time}.log`);
74099
+ const logPath = join15(dateDir, `${time}.log`);
73201
74100
  const structured = classifyError(error);
73202
74101
  const mem = process.memoryUsage ? process.memoryUsage() : undefined;
73203
74102
  const logEntry = {
@@ -73223,10 +74122,10 @@ function logError(error, contextInfo) {
73223
74122
  ` + JSON.stringify(logEntry, null, 2) + `
73224
74123
 
73225
74124
  `;
73226
- if (existsSync12(logPath)) {
74125
+ if (existsSync14(logPath)) {
73227
74126
  appendFileSync3(logPath, formatted, "utf-8");
73228
74127
  } else {
73229
- writeFileSync3(logPath, formatted, "utf-8");
74128
+ writeFileSync4(logPath, formatted, "utf-8");
73230
74129
  }
73231
74130
  return logPath;
73232
74131
  } catch {
@@ -110884,7 +111783,7 @@ function createProviderExecutedToolFactory2({
110884
111783
  supportsDeferredResults
110885
111784
  });
110886
111785
  }
110887
- async function resolve9(value) {
111786
+ async function resolve10(value) {
110888
111787
  if (typeof value === "function") {
110889
111788
  value = value();
110890
111789
  }
@@ -116110,7 +117009,7 @@ var XaiSpeechModel = class _XaiSpeechModel {
116110
117009
  const { requestBody, warnings, withTimestamps } = await this.getArgs(options);
116111
117010
  const { value, responseHeaders, rawValue } = await postJsonToApi4({
116112
117011
  url: `${this.config.baseURL}/tts`,
116113
- headers: combineHeaders4(this.config.headers ? await resolve9(this.config.headers) : undefined, options.headers),
117012
+ headers: combineHeaders4(this.config.headers ? await resolve10(this.config.headers) : undefined, options.headers),
116114
117013
  body: requestBody,
116115
117014
  failedResponseHandler: xaiFailedResponseHandler,
116116
117015
  successfulResponseHandler: withTimestamps ? createJsonResponseHandler4(xaiSpeechTimestampsResponseSchema) : createBinaryResponseHandler2(),
@@ -116917,288 +117816,59 @@ function inferProviderFromModelId(modelId) {
116917
117816
  }
116918
117817
  return null;
116919
117818
  }
116920
- // src/skills/discovery.ts
116921
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "node:fs";
116922
- import { homedir as homedir6 } from "node:os";
116923
- import { join as join14 } from "node:path";
116924
- function parseSkillFrontmatter(content) {
116925
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
116926
- if (!match || !match[1]) {
116927
- return null;
116928
- }
116929
- const rawYaml = match[1];
116930
- const lines = rawYaml.split(/\r?\n/);
116931
- const result = {};
116932
- let currentKey = null;
116933
- let descriptionLines = [];
116934
- for (const line of lines) {
116935
- const trimmed = line.trim();
116936
- if (!trimmed || trimmed.startsWith("#"))
116937
- continue;
116938
- const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
116939
- if (keyMatch && keyMatch[1]) {
116940
- const key = keyMatch[1].toLowerCase();
116941
- const val = (keyMatch[2] ?? "").trim();
116942
- if (key === "name") {
116943
- currentKey = "name";
116944
- result.name = unquote(val);
116945
- } else if (key === "description") {
116946
- currentKey = "description";
116947
- if (val === ">" || val === ">-" || val === "|" || val === "|-") {
116948
- descriptionLines = [];
116949
- } else if (val) {
116950
- descriptionLines = [unquote(val)];
116951
- } else {
116952
- descriptionLines = [];
116953
- }
116954
- } else {
116955
- currentKey = null;
116956
- }
116957
- } else if (currentKey === "description" && (line.startsWith(" ") || line.startsWith("\t"))) {
116958
- descriptionLines.push(trimmed);
116959
- }
116960
- }
116961
- if (descriptionLines.length > 0) {
116962
- result.description = descriptionLines.join(" ").trim();
116963
- }
116964
- return result;
116965
- }
116966
- function unquote(str) {
116967
- const trimmed = str.trim();
116968
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
116969
- return trimmed.slice(1, -1);
116970
- }
116971
- return trimmed;
116972
- }
116973
- function discoverSkills(cwd = process.cwd()) {
116974
- const targets = [
116975
- { dir: join14(cwd, ".agents", "skills"), source: "workspace" },
116976
- { dir: join14(homedir6(), ".agents", "skills"), source: "user-agents" },
116977
- { dir: join14(homedir6(), ".steward", "skills"), source: "user-steward" }
116978
- ];
116979
- const skillMap = new Map;
116980
- for (const target of targets) {
116981
- if (!existsSync13(target.dir)) {
116982
- continue;
116983
- }
116984
- try {
116985
- const entries = readdirSync5(target.dir, { withFileTypes: true });
116986
- for (const entry of entries) {
116987
- if (!entry.isDirectory())
116988
- continue;
116989
- const skillDir = join14(target.dir, entry.name);
116990
- const skillMdPath = join14(skillDir, "SKILL.md");
116991
- const fallbackMdPath = join14(skillDir, "skill.md");
116992
- const resolvedFile = existsSync13(skillMdPath) ? skillMdPath : existsSync13(fallbackMdPath) ? fallbackMdPath : null;
116993
- if (!resolvedFile)
116994
- continue;
116995
- try {
116996
- const raw = readFileSync12(resolvedFile, "utf-8");
116997
- const frontmatter = parseSkillFrontmatter(raw);
116998
- const skillName = frontmatter?.name || entry.name;
116999
- const description = frontmatter?.description || "No description provided.";
117000
- if (!skillMap.has(skillName)) {
117001
- skillMap.set(skillName, {
117002
- name: skillName,
117003
- description,
117004
- filePath: resolvedFile,
117005
- dirPath: skillDir,
117006
- source: target.source
117007
- });
117008
- }
117009
- } catch {}
117010
- }
117011
- } catch {}
117012
- }
117013
- return Array.from(skillMap.values());
117014
- }
117015
- function formatSkillsForSystemPrompt(skills) {
117016
- if (skills.length === 0)
117017
- return "";
117018
- const list = skills.map((s) => `- ${s.name} (${s.filePath}): ${s.description}`).join(`
117019
- `);
117020
- return `<skills>
117021
- You can use specialized 'skills' to help you with complex tasks.
117022
-
117023
- Skills are folders of instructions, scripts, and resources that extend your capabilities for specialized tasks. Each skill folder contains a SKILL.md file with YAML frontmatter and detailed markdown instructions.
117024
117819
 
117025
- Available skills:
117026
- ${list}
117027
-
117028
- If a skill seems relevant to your current task, you MUST read its SKILL.md instructions using read_file before proceeding.
117029
- </skills>`;
117030
- }
117031
117820
  // src/engine/system-prompt.ts
117032
117821
  function buildSystemPrompt(options = {}) {
117033
117822
  const cwd = options.cwd ?? process.cwd();
117034
117823
  const platform = process.platform;
117035
- const skills = options.skills ?? discoverSkills(cwd);
117036
117824
  const dateUTC = new Date().toUTCString();
117037
- let isAGitREPO = "no";
117038
- try {
117039
- isAGitREPO = Bun.spawnSync(["git", "status"], { cwd, stdio: ["ignore", "ignore", "ignore"] }).exitCode === 0 ? "yes" : "no";
117040
- } catch {
117041
- isAGitREPO = "no";
117042
- }
117043
- let prompt = `
117044
- You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
117045
-
117046
- IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously. Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation.
117047
- NOTE: - To give feedback, users should report the issue at https://github.com/sapirrior/steward/issues
117048
-
117049
- NOTE: When the user directly asks about Steward (eg 'can Steward do...', 'does Steward have...') or asks in second person (eg 'are you able...', 'can you do...', 'will you be abel to do...'), response the USER to head over to the Open-Source repo of Steward (https://github.com/sapirrior/steward/) for information related to steward and its architecture
117050
-
117051
- # Tone and style
117052
- You should be concise, direct, and to the point.
117053
- You MUST answer concisely with fewer than 4 lines (not including the tool use or code generation), unless the USER asks for detail.
117054
- Do not add additional code explanation summary unless requested by the USER. After working on a file, just stop, rather than providing an explanation of what you did.
117055
- IMPORTANT: You should minimize the output tokens as much as possible while maintaining appropriate helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-4 sentences or a short paragraph, please do.
117056
- IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you explicitly.
117057
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
117058
- Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
117059
-
117060
- <example>
117061
- user: what is 2 + 2
117062
- assistant: 4
117063
- </example>
117064
-
117065
- <example>
117066
- user: 4 + 4
117067
- assistant: 8
117068
- </example>
117069
-
117070
- <example>
117071
- user: what command should I run to read the content of a specific file?
117072
- assistant: cat
117073
- </example>
117074
-
117075
- <example>
117076
- user: what command should I run to watch files in the current directory?
117077
- [assistant runs the ls command through tool call to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
117078
- assistant: npm run dev
117079
- </example>
117080
-
117081
- <example>
117082
- user: can you find all the TODO comments in src/
117083
- [assistant runs search text tool call and sees TODO comment occurance in src/foo.c at line 11 src/boo.c at 180]
117084
- assistant: src/foo:11 and src/boo.c:180
117085
- </example>
117825
+ let prompt = `You are Steward, an interactive software-engineering agent operating in a terminal workspace. Assist the user with building, refactoring, debugging, inspecting, and verifying software.
117086
117826
 
117087
- When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the USER understands what you are doing (this is especially very important when you are running a command that will make changes to the USER's system including file mutation).
117088
- Output text to communicate with the USER; all text you output outside of tool use is displayed to the USER. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the USER during the session.
117089
- If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
117090
- IMPORTANT: Only use emojis if the USER explicitly requests it. Avoid using emojis in all communication unless asked.
117091
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
117827
+ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code intended for malicious use.
117092
117828
 
117093
- # Proactiveness
117094
- You are allowed to be proactive, but only when the USER asks you to do something. You should strive to strike a balance between:
117095
- - Doing the right thing when asked, including taking actions and follow-up actions
117096
- - Not surprising the USER with actions you take without asking
117097
- For example, if the USER asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
117829
+ # Operating Principles
117830
+ - Inspect before editing: investigate existing files, directories, and patterns before making changes.
117831
+ - Minimal and idiomatic: prefer the smallest correct change that aligns with existing repository architecture and conventions.
117832
+ - Never assume external dependencies or libraries exist; check project configuration files (e.g. package.json, Cargo.toml, pyproject.toml) first.
117833
+ - Security and secrets: never expose, commit, or log API keys, credentials, or secrets.
117834
+ - Verify your changes: run relevant project verification or tests after making non-trivial modifications.
117835
+ - Never commit git changes unless the user explicitly requests you to commit.
117836
+ - Provide direct, concise responses suitable for a terminal interface. Explain non-obvious failures or decisions clearly without unnecessary filler or commentary.
117837
+ - When referencing code locations, use the format: \`file_path:line_number\`.
117838
+ - Avoid adding redundant comments unless explaining non-obvious invariants or explicitly requested.
117098
117839
 
117099
- # Following conventions
117100
- When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
117101
- - NEVER assume that a given library is available, even if it is well known or. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml or pyproject.toml, and so on depending on the language).
117102
- - When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
117103
- - When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
117104
- - Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
117105
- - Always read the AI Context File / Memory File / Contributing Guidelines / readme for better understanding of the codebase, code writing styles or conventions. For example (not limited to): AGENTS.md, IMPORTANT.md, CONTRIBUTING.md, readme.txt / README.md.
117840
+ # Tool Usage Policy
117841
+ - Investigation: use read_file, grep, list_dir, and glob to explore code before modifying it.
117842
+ - Mutations: use write_file and edit_file to apply changes. Respect user permission prompts.
117843
+ - Commands: when using bash, you MUST always provide a concise, non-empty 'explanation' parameter describing what the command does and why it is needed.
117844
+ - Background tasks: use task_read to check status/output, task_send_input to send standard input (with a trailing newline), and task_kill to terminate.
117845
+ - Web: use web_fetch and web_search when external documentation or live data is needed. If web_fetch indicates a redirect, follow up with the target URL.
117106
117846
 
117107
- # Code style
117108
- - IMPORTANT: DO NOT ADD **ANY** COMMENTS unless explicitly asked.
117847
+ # Task and Plan Management (Todos)
117848
+ - For multi-step, complex, or verification-heavy tasks, maintain operational state using session todos:
117849
+ - TodoWrite: initialize or replace the session todo list (must contain 2 to 10 items).
117850
+ - TodoUpdate: update exactly one todo item status or description as progress occurs.
117851
+ - TodoRead: inspect current session todos when recovering or verifying progress.
117852
+ - Keep at most one item in 'in_progress' status at any time.
117853
+ - Mark todos completed immediately after completing each step; do not batch progress updates.
117854
+ - For simple, one-step queries or edits, do not create a todo list.
117109
117855
 
117110
- # Tool usage policy
117111
- - Use the edit file and write file tools for both normal code/file changes as well as making plans and todos.
117112
- - When WebFetch returns a message about a redirect to a different host, immediately make a new WebFetch request with the redirect URL provided in the response.
117113
- - When web search returns but does not contain enough data, use the URLs from the results and WebFetch them for more detailed information.
117114
- - When a command moves to the background as a shell task, use task_read to inspect its status/output, task_send_input to send standard input (always include a trailing '
117115
- ' to submit a line / press Enter), and task_kill to terminate it.
117116
-
117117
- # Task Management (Planning)
117118
- You have access to the write file and edit file tools to help you manage and plan tasks alongside normal file/code creation. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
117119
- These tools are also EXTREMELY helpful for planning tasks and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks — and that is unacceptable.
117120
- IMPORTANT: Write plans/tasks in the codebase/repository's .steward/plans/ directory and write them in standard Markdown format. For example, a terminal TUI library plan: [project dir]/.steward/plans/tui_library_plan.md, an HTTP API server plan: [project dir]/.steward/plans/api_server_implementation_plan.md.
117121
- IMPORTANT: If the user asks to create the plan in a different directory than the standard [project dir]/.steward/plans/, proceed normally but warn the USER that it is not recommended to make plans outside the .steward/plans/ directory.
117122
- IMPORTANT: Always use proper names for the plan/task files (wrong: plan.md, plan_12.md, implementation.md; right: code_refactor_plan.md, codebase_migration_plan.md).
117123
- IMPORTANT: It is **VERY VERY IMPORTANT** to always put a todo list between the main title header and the content of the plan. It is critical that you mark todos as completed as soon as you are done with a task. **NEVER** batch up multiple tasks before marking them as completed.
117124
-
117125
- <example>
117126
- user: Run the build and fix any type errors.
117127
- assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
117128
- - Run the build
117129
- - Fix any type errors
117130
- [assistant uses write_file to write the task todo/plan in [project dir]/.steward/plans/fixing_type_errors_plan.md]
117131
- assistant: I'm now going to run the build using Bash.
117132
- [assistant runs the build command and gets 10 type errors]
117133
- assistant: Looks like I found 10 type errors. I'm going to use the edit file tool to write 10 items to the todo list.
117134
- [assistant runs edit tool and puts 10 child todos under the 'figure out type error files' todo in [project dir]/.steward/plans/fixing_type_errors_plan.md]
117135
- assistant: Marking the first todo as in_progress.
117136
- [assistant uses the edit file to mark 'run build command to find type errors' todo]
117137
- Let me start working on the first item...
117138
- [assistant reads the first file, uses edit tool and fixes it]
117139
- The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
117140
- [same as first one, assistant fixes all files]
117141
- </example>
117142
- In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build to double-check the fixes.
117143
-
117144
- <example>
117145
- user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
117146
-
117147
- assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the write file tool to plan this task.
117148
- Adding the following todos to the todo list:
117149
- 1. Research existing metrics tracking in the codebase
117150
- 2. Design the metrics collection system
117151
- 3. Implement core metrics tracking functionality
117152
- 4. Create export functionality for different formats
117153
- [assistant uses the write file to write the plan and its todos in [project dir]/.steward/plans/]
117154
- Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
117155
- [assistant lists all files and reads all the relevant files step by step]
117156
- I'm going to search for any existing metrics or telemetry code in the project.
117157
- [assistant runs search text to find any existing telemetry code in the codebase]
117158
- I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
117159
- [assistant continues implementing the feature step by step, marking todos as completed as they go]
117160
- </example>
117161
-
117162
- IMPORTANT: Completed todo: marked with ◉ beside the todo text and * beside the current working todo.
117163
-
117164
- # Doing tasks
117165
- The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
117166
- - Use the write file tool to plan the task if required.
117167
- - Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively, both in parallel and sequentially.
117168
- - Implement the solution using all tools available to you.
117169
- - Verify the solution if possible with tests. NEVER assume a specific test framework or test script. Check the README or search the codebase to determine the testing approach.
117170
- - VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
117171
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
117172
-
117173
- Here is useful information about the environment you are running in:
117174
- <env>
117175
- Working directory: ${cwd}
117176
- Is directory a git repo: ${isAGitREPO}
117177
- Platform: ${platform}
117178
- Today's date: ${dateUTC}
117179
- </env>
117180
-
117181
- # Code References
117182
- When referencing specific functions or pieces of code, include the pattern \`file_path:line_number\` to allow the user to easily navigate to the source code location.
117183
-
117184
- <example>
117185
- user: Where are errors from the client handled?
117186
- assistant: Clients are marked as failed in the \`connectToServer\` function in src/services/process.ts:712.
117187
- </example>`;
117188
- const skillsPrompt = formatSkillsForSystemPrompt(skills);
117189
- if (skillsPrompt) {
117190
- prompt += `
117856
+ # Specialized Skills Policy
117857
+ - Skills are discoverable on-demand packages of specialized instructions located in workspace or user configuration.
117858
+ - Use SkillList to discover available skills and their descriptions.
117859
+ - Use SkillRead to load a skill's instructions (SKILL.md) or relative resources only when a task requires specialized domain guidance.
117860
+ - Skill instructions are supplemental and must not override core safety rules, tool boundaries, or user permission constraints.
117191
117861
 
117192
- ${skillsPrompt}`;
117193
- }
117862
+ <env>
117863
+ Working directory: ${cwd}
117864
+ Platform: ${platform}
117865
+ Today's date: ${dateUTC}
117866
+ </env>`;
117194
117867
  if (options.userRules && options.userRules.length > 0) {
117195
117868
  prompt += `
117196
117869
 
117197
117870
  <user_defined_rules>
117198
- The following rules are set by the user and MUST be followed without exception.
117199
- They take precedence over all other style and behaviour guidelines above,
117200
- except for the operating_principles and security sections.
117201
-
117871
+ The following rules are configured by the user and must be followed:
117202
117872
  ${options.userRules.map((rule) => `- ${rule}`).join(`
117203
117873
  `)}
117204
117874
  </user_defined_rules>`;
@@ -117336,7 +118006,7 @@ class AgentSession {
117336
118006
  this.messages.push(userMessage);
117337
118007
  this.isGenerating = true;
117338
118008
  this.activeAbortController = new AbortController;
117339
- const turnId = randomUUID7();
118009
+ const turnId = randomUUID8();
117340
118010
  const turnStartedAt = new Date().toISOString();
117341
118011
  const turnStartMonotonic = performance.now();
117342
118012
  const turnModel = this.getModel();
@@ -117362,6 +118032,7 @@ class AgentSession {
117362
118032
  await tracker.beginTurn(turnId, this.sessionData.turns.length + 1);
117363
118033
  const toolContext = {
117364
118034
  cwd,
118035
+ sessionId: this.sessionData.id,
117365
118036
  abortSignal: this.activeAbortController.signal,
117366
118037
  checkpointTracker: tracker,
117367
118038
  mutationLocks: globalMutationLockManager,
@@ -117378,6 +118049,7 @@ class AgentSession {
117378
118049
  activeTools = defaultToolCatalog.toAISDKTools(toolContext);
117379
118050
  }
117380
118051
  const instructions = buildSystemPrompt({
118052
+ cwd,
117381
118053
  extraInstructions: options.extraInstructions
117382
118054
  });
117383
118055
  const wrappedOnEvent = (event) => {
@@ -118829,6 +119501,7 @@ class StatusBar extends Component {
118829
119501
  clip = true;
118830
119502
  warningTimer = null;
118831
119503
  voiceTimer = null;
119504
+ updateTimer = null;
118832
119505
  constructor(props) {
118833
119506
  super(props);
118834
119507
  this.state = {
@@ -118838,7 +119511,8 @@ class StatusBar extends Component {
118838
119511
  exitPending: props.exitPending,
118839
119512
  warning: props.warning,
118840
119513
  voiceState: props.voiceState ?? "idle",
118841
- voiceDurationSec: props.voiceDurationSec ?? 0
119514
+ voiceDurationSec: props.voiceDurationSec ?? 0,
119515
+ updateStatus: props.updateStatus
118842
119516
  };
118843
119517
  }
118844
119518
  update(partial) {
@@ -118861,6 +119535,21 @@ class StatusBar extends Component {
118861
119535
  });
118862
119536
  }
118863
119537
  }
119538
+ setUpdateStatus(updateStatus, durationMs) {
119539
+ if (this.updateTimer) {
119540
+ clearTimeout(this.updateTimer);
119541
+ this.updateTimer = null;
119542
+ }
119543
+ this.setState({ updateStatus });
119544
+ if (updateStatus && durationMs && durationMs > 0) {
119545
+ this.updateTimer = setTimeout(() => {
119546
+ this.updateTimer = null;
119547
+ if (this.state.updateStatus === updateStatus) {
119548
+ this.setState({ updateStatus: undefined });
119549
+ }
119550
+ }, durationMs);
119551
+ }
119552
+ }
118864
119553
  showWarning(warningText, durationMs = 4000) {
118865
119554
  if (this.warningTimer) {
118866
119555
  clearTimeout(this.warningTimer);
@@ -118883,12 +119572,25 @@ class StatusBar extends Component {
118883
119572
  clearInterval(this.voiceTimer);
118884
119573
  this.voiceTimer = null;
118885
119574
  }
119575
+ if (this.updateTimer) {
119576
+ clearTimeout(this.updateTimer);
119577
+ this.updateTimer = null;
119578
+ }
118886
119579
  }
118887
119580
  render(width) {
118888
119581
  const theme = getTheme();
118889
119582
  const termWidth = width ?? process.stdout.columns ?? 80;
118890
119583
  const maxCols = Math.max(1, termWidth);
118891
- const { model, usage, isBusy, exitPending, warning, voiceState, voiceDurationSec } = this.state;
119584
+ const {
119585
+ model,
119586
+ usage,
119587
+ isBusy,
119588
+ exitPending,
119589
+ warning,
119590
+ voiceState,
119591
+ voiceDurationSec,
119592
+ updateStatus
119593
+ } = this.state;
118892
119594
  let left = "";
118893
119595
  if (warning) {
118894
119596
  left = themeColor2(theme.warning)(warning);
@@ -118904,6 +119606,20 @@ class StatusBar extends Component {
118904
119606
  } else if (exitPending) {
118905
119607
  const errColor = themeColor2(theme.error);
118906
119608
  left = `${errColor("▸ ")}${source_default.dim("Press ")}${errColor("Ctrl+C")}${source_default.dim(" again to exit")}`;
119609
+ } else if (updateStatus && updateStatus.state !== "idle") {
119610
+ if (updateStatus.state === "checking") {
119611
+ left = source_default.white(updateStatus.message ?? "Checking for updates...");
119612
+ } else if (updateStatus.state === "available") {
119613
+ left = source_default.white(updateStatus.message ?? `Update v${updateStatus.version ?? ""} is available`);
119614
+ } else if (updateStatus.state === "no-updates") {
119615
+ left = source_default.white(updateStatus.message ?? "Steward is up to date");
119616
+ } else if (updateStatus.state === "error" && updateStatus.message) {
119617
+ left = themeColor2(theme.error)(updateStatus.message);
119618
+ } else if (isBusy) {
119619
+ left = source_default.dim("esc to interrupt");
119620
+ } else {
119621
+ left = source_default.dim("? for shortcuts");
119622
+ }
118907
119623
  } else if (isBusy) {
118908
119624
  left = source_default.dim("esc to interrupt");
118909
119625
  } else {
@@ -119037,6 +119753,8 @@ class StreamingView extends Component {
119037
119753
  const maxArgLen = Math.max(10, maxCols - toolName.length - 8);
119038
119754
  const truncatedArg = targetArg.length > maxArgLen ? truncateMiddle(targetArg, maxArgLen) : targetArg;
119039
119755
  line += `${source_default.dim("(")}${source_default.dim(truncatedArg)}${source_default.dim(")")}`;
119756
+ } else {
119757
+ line += `${source_default.dim("()")}`;
119040
119758
  }
119041
119759
  lines.push(line);
119042
119760
  if (activeTool.recentLines && activeTool.recentLines.length > 0) {
@@ -120545,235 +121263,6 @@ class BashPermissionDock extends Component {
120545
121263
  }
120546
121264
  }
120547
121265
 
120548
- // src/utils/diff.ts
120549
- function tokenizeLines(content) {
120550
- if (!content)
120551
- return [];
120552
- const tokens = [];
120553
- const regex = /([^\r\n]*)(\r?\n)?/g;
120554
- let match;
120555
- while ((match = regex.exec(content)) !== null) {
120556
- if (match.index === content.length)
120557
- break;
120558
- const text = match[1] ?? "";
120559
- const ending = match[2] ?? "";
120560
- tokens.push({ text, ending });
120561
- if (!ending && match.index + text.length === content.length)
120562
- break;
120563
- }
120564
- return tokens;
120565
- }
120566
- function tokenizeWords(text) {
120567
- if (!text)
120568
- return [];
120569
- const matches = text.match(/[\w]+|[^\w\s]+|\s+/g);
120570
- return matches ?? [text];
120571
- }
120572
- function myersDiff(oldArr, newArr, equals = (a, b) => a === b) {
120573
- const n = oldArr.length;
120574
- const m = newArr.length;
120575
- if (n === 0 && m === 0)
120576
- return [];
120577
- if (n === 0)
120578
- return newArr.map((newItem, i) => ({ kind: "insert", newItem, newIndex: i }));
120579
- if (m === 0)
120580
- return oldArr.map((oldItem, i) => ({ kind: "delete", oldItem, oldIndex: i }));
120581
- const max = n + m;
120582
- const v = { 1: 0 };
120583
- const trace = [];
120584
- for (let d = 0;d <= max; d++) {
120585
- trace.push({ ...v });
120586
- for (let k = -d;k <= d; k += 2) {
120587
- let x = k === -d || k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1) ? v[k + 1] ?? 0 : (v[k - 1] ?? 0) + 1;
120588
- let y = x - k;
120589
- while (x < n && y < m && equals(oldArr[x], newArr[y])) {
120590
- x++;
120591
- y++;
120592
- }
120593
- v[k] = x;
120594
- if (x >= n && y >= m) {
120595
- const ops = [];
120596
- let cx = n;
120597
- let cy = m;
120598
- for (let cd = d;cd > 0; cd--) {
120599
- const prevV = trace[cd];
120600
- const pk = cx - cy;
120601
- const prevK = pk === -cd || pk !== cd && (prevV[pk - 1] ?? -1) < (prevV[pk + 1] ?? -1) ? pk + 1 : pk - 1;
120602
- const prevX = prevV[prevK] ?? 0;
120603
- const prevY = prevX - prevK;
120604
- while (cx > prevX + (prevK === pk + 1 ? 0 : 1) && cy > prevY + (prevK === pk + 1 ? 1 : 0)) {
120605
- cx--;
120606
- cy--;
120607
- ops.push({
120608
- kind: "equal",
120609
- oldItem: oldArr[cx],
120610
- newItem: newArr[cy],
120611
- oldIndex: cx,
120612
- newIndex: cy
120613
- });
120614
- }
120615
- if (prevK === pk + 1) {
120616
- cy--;
120617
- ops.push({ kind: "insert", newItem: newArr[cy], newIndex: cy });
120618
- } else {
120619
- cx--;
120620
- ops.push({ kind: "delete", oldItem: oldArr[cx], oldIndex: cx });
120621
- }
120622
- }
120623
- while (cx > 0 && cy > 0) {
120624
- cx--;
120625
- cy--;
120626
- ops.push({
120627
- kind: "equal",
120628
- oldItem: oldArr[cx],
120629
- newItem: newArr[cy],
120630
- oldIndex: cx,
120631
- newIndex: cy
120632
- });
120633
- }
120634
- ops.reverse();
120635
- return ops;
120636
- }
120637
- }
120638
- }
120639
- return [];
120640
- }
120641
- function computeWordDiff(oldLine, newLine) {
120642
- const oldWords = tokenizeWords(oldLine);
120643
- const newWords = tokenizeWords(newLine);
120644
- const ops = myersDiff(oldWords, newWords);
120645
- const oldSpans = [];
120646
- const newSpans = [];
120647
- for (const op of ops) {
120648
- if (op.kind === "equal") {
120649
- const text = op.oldItem ?? "";
120650
- oldSpans.push({ text, kind: "equal" });
120651
- newSpans.push({ text, kind: "equal" });
120652
- } else if (op.kind === "delete") {
120653
- oldSpans.push({ text: op.oldItem ?? "", kind: "deletion" });
120654
- } else if (op.kind === "insert") {
120655
- newSpans.push({ text: op.newItem ?? "", kind: "addition" });
120656
- }
120657
- }
120658
- return { oldSpans, newSpans };
120659
- }
120660
- function buildUnifiedDiff(before, after, contextLines = 3) {
120661
- const oldTokens = tokenizeLines(before);
120662
- const newTokens = tokenizeLines(after);
120663
- if (before === after) {
120664
- return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
120665
- }
120666
- const ops = myersDiff(oldTokens, newTokens, (a, b) => a.text === b.text && a.ending === b.ending);
120667
- const editIndices = ops.map((op, i) => op.kind !== "equal" ? i : -1).filter((i) => i !== -1);
120668
- if (editIndices.length === 0) {
120669
- return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
120670
- }
120671
- const hunkRanges = [];
120672
- let currentRange = {
120673
- startIdx: Math.max(0, editIndices[0] - contextLines),
120674
- endIdx: Math.min(ops.length - 1, editIndices[0] + contextLines)
120675
- };
120676
- for (let i = 1;i < editIndices.length; i++) {
120677
- const editIdx = editIndices[i];
120678
- const opStart = Math.max(0, editIdx - contextLines);
120679
- const opEnd = Math.min(ops.length - 1, editIdx + contextLines);
120680
- if (opStart <= currentRange.endIdx) {
120681
- currentRange.endIdx = Math.max(currentRange.endIdx, opEnd);
120682
- } else {
120683
- hunkRanges.push(currentRange);
120684
- currentRange = { startIdx: opStart, endIdx: opEnd };
120685
- }
120686
- }
120687
- hunkRanges.push(currentRange);
120688
- const hunks = [];
120689
- for (const range of hunkRanges) {
120690
- const lines = [];
120691
- let oldCount = 0;
120692
- let newCount = 0;
120693
- let oldStart = null;
120694
- let newStart = null;
120695
- for (let i = range.startIdx;i <= range.endIdx; i++) {
120696
- const op = ops[i];
120697
- if (op.kind === "equal") {
120698
- const oIdx = op.oldIndex + 1;
120699
- const nIdx = op.newIndex + 1;
120700
- if (oldStart === null)
120701
- oldStart = oIdx;
120702
- if (newStart === null)
120703
- newStart = nIdx;
120704
- lines.push({
120705
- kind: "context",
120706
- text: op.oldItem.text,
120707
- ending: op.oldItem.ending,
120708
- oldLineNumber: oIdx,
120709
- newLineNumber: nIdx
120710
- });
120711
- oldCount++;
120712
- newCount++;
120713
- } else if (op.kind === "delete") {
120714
- const oIdx = op.oldIndex + 1;
120715
- if (oldStart === null)
120716
- oldStart = oIdx;
120717
- lines.push({
120718
- kind: "deletion",
120719
- text: op.oldItem.text,
120720
- ending: op.oldItem.ending,
120721
- oldLineNumber: oIdx
120722
- });
120723
- oldCount++;
120724
- } else if (op.kind === "insert") {
120725
- const nIdx = op.newIndex + 1;
120726
- if (newStart === null)
120727
- newStart = nIdx;
120728
- lines.push({
120729
- kind: "addition",
120730
- text: op.newItem.text,
120731
- ending: op.newItem.ending,
120732
- newLineNumber: nIdx
120733
- });
120734
- newCount++;
120735
- }
120736
- }
120737
- let l = 0;
120738
- while (l < lines.length) {
120739
- if (lines[l]?.kind === "deletion") {
120740
- const delStart = l;
120741
- while (l < lines.length && lines[l]?.kind === "deletion")
120742
- l++;
120743
- const delEnd = l;
120744
- const addStart = l;
120745
- while (l < lines.length && lines[l]?.kind === "addition")
120746
- l++;
120747
- const addEnd = l;
120748
- const delCount = delEnd - delStart;
120749
- const addCount = addEnd - addStart;
120750
- const pairCount = Math.min(delCount, addCount);
120751
- for (let p = 0;p < pairCount; p++) {
120752
- const delLine = lines[delStart + p];
120753
- const addLine = lines[addStart + p];
120754
- const { oldSpans, newSpans } = computeWordDiff(delLine.text, addLine.text);
120755
- delLine.spans = oldSpans;
120756
- addLine.spans = newSpans;
120757
- }
120758
- } else {
120759
- l++;
120760
- }
120761
- }
120762
- hunks.push({
120763
- oldStart: oldStart ?? (oldTokens.length === 0 ? 0 : 1),
120764
- oldCount,
120765
- newStart: newStart ?? (newTokens.length === 0 ? 0 : 1),
120766
- newCount,
120767
- lines
120768
- });
120769
- }
120770
- return {
120771
- hunks,
120772
- oldLineCount: oldTokens.length,
120773
- newLineCount: newTokens.length
120774
- };
120775
- }
120776
-
120777
121266
  // src/tui/components/docks/FilePermissionDock.tsx
120778
121267
  class FilePermissionDock extends Component {
120779
121268
  wrap = true;
@@ -120903,8 +121392,7 @@ class FilePermissionDock extends Component {
120903
121392
  const wrapped = prefixedBlock(firstPrefix, bodyText, {
120904
121393
  continuationPrefix: contPrefix,
120905
121394
  width: maxCols,
120906
- bg: diffDeleteBg,
120907
- padToWidth: true
121395
+ bg: diffDeleteBg
120908
121396
  });
120909
121397
  renderedRows.push(...wrapped);
120910
121398
  } else if (line.kind === "addition") {
@@ -120915,8 +121403,7 @@ class FilePermissionDock extends Component {
120915
121403
  const wrapped = prefixedBlock(firstPrefix, bodyText, {
120916
121404
  continuationPrefix: contPrefix,
120917
121405
  width: maxCols,
120918
- bg: diffAddBg,
120919
- padToWidth: true
121406
+ bg: diffAddBg
120920
121407
  });
120921
121408
  renderedRows.push(...wrapped);
120922
121409
  } else {
@@ -122049,6 +122536,163 @@ class VoiceController {
122049
122536
  this.setState("idle");
122050
122537
  }
122051
122538
  }
122539
+ // src/services/updater/service.ts
122540
+ var DEFAULT_REPO = "sapirrior/steward";
122541
+ function parseSemver(v) {
122542
+ if (!v || typeof v !== "string") {
122543
+ return [0, 0, 0];
122544
+ }
122545
+ const clean = v.replace(/^v/i, "").trim().split("-")[0] ?? "";
122546
+ const parts = clean.split(".").map((p) => parseInt(p, 10) || 0);
122547
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
122548
+ }
122549
+ function isNewerVersion(current, latest) {
122550
+ const [cMaj, cMin, cPat] = parseSemver(current);
122551
+ const [lMaj, lMin, lPat] = parseSemver(latest);
122552
+ if (lMaj > cMaj)
122553
+ return true;
122554
+ if (lMaj < cMaj)
122555
+ return false;
122556
+ if (lMin > cMin)
122557
+ return true;
122558
+ if (lMin < cMin)
122559
+ return false;
122560
+ return lPat > cPat;
122561
+ }
122562
+
122563
+ class UpdateCheckerService {
122564
+ currentVersion;
122565
+ repo;
122566
+ state = "idle";
122567
+ options;
122568
+ checkTimer = null;
122569
+ resetTimer = null;
122570
+ constructor(options = {}) {
122571
+ this.options = options;
122572
+ this.currentVersion = options.currentVersion || package_default.version || "0.0.0";
122573
+ this.repo = options.repo || DEFAULT_REPO;
122574
+ }
122575
+ getState() {
122576
+ return this.state;
122577
+ }
122578
+ getRepo() {
122579
+ return this.repo;
122580
+ }
122581
+ getCurrentVersion() {
122582
+ return this.currentVersion;
122583
+ }
122584
+ get rawPackageJsonUrl() {
122585
+ return `https://raw.githubusercontent.com/${this.repo}/main/package.json`;
122586
+ }
122587
+ setState(state, info) {
122588
+ this.state = state;
122589
+ this.options.onStatusChange?.(state, info);
122590
+ }
122591
+ async checkForUpdates() {
122592
+ if (process.env.STEWARD_NO_UPDATE_CHECK === "1" || process.env.STEWARD_NO_UPDATE_CHECK === "true") {
122593
+ return null;
122594
+ }
122595
+ if (this.resetTimer) {
122596
+ clearTimeout(this.resetTimer);
122597
+ this.resetTimer = null;
122598
+ }
122599
+ try {
122600
+ this.setState("checking", { message: "Checking for updates..." });
122601
+ const checkStartTime = Date.now();
122602
+ const controller = new AbortController;
122603
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 1e4);
122604
+ const response = await fetch(this.rawPackageJsonUrl, {
122605
+ headers: {
122606
+ "User-Agent": `steward-cli/${this.currentVersion}`,
122607
+ Accept: "application/json"
122608
+ },
122609
+ signal: controller.signal
122610
+ });
122611
+ clearTimeout(timeout);
122612
+ const elapsed = Date.now() - checkStartTime;
122613
+ const targetMinDisplay = this.options.minDisplayMs !== undefined ? this.options.minDisplayMs : 1000;
122614
+ const minDisplayDelay = Math.max(0, targetMinDisplay - elapsed);
122615
+ if (minDisplayDelay > 0) {
122616
+ await new Promise((resolve) => setTimeout(resolve, minDisplayDelay));
122617
+ }
122618
+ if (!response.ok) {
122619
+ this.setState("error", { message: "Failed to check for updates" });
122620
+ this.resetTimer = setTimeout(() => {
122621
+ if (this.state === "error")
122622
+ this.setState("idle");
122623
+ }, 3000);
122624
+ return null;
122625
+ }
122626
+ const data = await response.json();
122627
+ if (!data || typeof data !== "object" || typeof data.version !== "string") {
122628
+ this.setState("error", { message: "Invalid upstream version response" });
122629
+ this.resetTimer = setTimeout(() => {
122630
+ if (this.state === "error")
122631
+ this.setState("idle");
122632
+ }, 3000);
122633
+ return null;
122634
+ }
122635
+ const latestVersion = data.version.replace(/^v/i, "").trim();
122636
+ if (!latestVersion) {
122637
+ this.setState("error", { message: "Empty upstream version response" });
122638
+ this.resetTimer = setTimeout(() => {
122639
+ if (this.state === "error")
122640
+ this.setState("idle");
122641
+ }, 3000);
122642
+ return null;
122643
+ }
122644
+ const hasUpdate = isNewerVersion(this.currentVersion, latestVersion);
122645
+ const updateInfo = {
122646
+ currentVersion: this.currentVersion,
122647
+ latestVersion,
122648
+ hasUpdate
122649
+ };
122650
+ if (hasUpdate) {
122651
+ this.setState("available", {
122652
+ version: latestVersion,
122653
+ message: `Update v${latestVersion} is available`
122654
+ });
122655
+ } else {
122656
+ this.setState("no-updates", {
122657
+ version: this.currentVersion,
122658
+ message: `Steward is up to date (v${this.currentVersion})`
122659
+ });
122660
+ this.resetTimer = setTimeout(() => {
122661
+ if (this.state === "no-updates")
122662
+ this.setState("idle");
122663
+ }, 3500);
122664
+ }
122665
+ return updateInfo;
122666
+ } catch (err) {
122667
+ logError(err, { component: "UpdateCheckerService", method: "checkForUpdates" });
122668
+ this.setState("error", { message: "Failed to check for updates" });
122669
+ this.resetTimer = setTimeout(() => {
122670
+ if (this.state === "error")
122671
+ this.setState("idle");
122672
+ }, 3000);
122673
+ return null;
122674
+ }
122675
+ }
122676
+ startBackgroundCheck(initialDelayMs = 2000) {
122677
+ if (this.checkTimer) {
122678
+ clearTimeout(this.checkTimer);
122679
+ }
122680
+ this.checkTimer = setTimeout(async () => {
122681
+ this.checkTimer = null;
122682
+ await this.checkForUpdates();
122683
+ }, initialDelayMs);
122684
+ }
122685
+ dispose() {
122686
+ if (this.checkTimer) {
122687
+ clearTimeout(this.checkTimer);
122688
+ this.checkTimer = null;
122689
+ }
122690
+ if (this.resetTimer) {
122691
+ clearTimeout(this.resetTimer);
122692
+ this.resetTimer = null;
122693
+ }
122694
+ }
122695
+ }
122052
122696
  // src/tui/app.ts
122053
122697
  class TUIApp {
122054
122698
  engine;
@@ -122064,6 +122708,7 @@ class TUIApp {
122064
122708
  ctrlCTimer = null;
122065
122709
  isBusy = false;
122066
122710
  voiceController;
122711
+ updateChecker;
122067
122712
  permissionQueue;
122068
122713
  constructor(options = {}) {
122069
122714
  this.cwd = options.cwd ?? process.cwd();
@@ -122126,6 +122771,32 @@ class TUIApp {
122126
122771
  this.statusBar.showWarning(warning);
122127
122772
  }
122128
122773
  });
122774
+ this.updateChecker = new UpdateCheckerService({
122775
+ currentVersion: options.version,
122776
+ onStatusChange: (state, info) => {
122777
+ if (state === "idle") {
122778
+ this.statusBar.setUpdateStatus(undefined);
122779
+ } else if (state === "available") {
122780
+ this.statusBar.setUpdateStatus({
122781
+ state,
122782
+ version: info?.version,
122783
+ message: info?.message
122784
+ }, 8000);
122785
+ } else if (state === "error") {
122786
+ this.statusBar.setUpdateStatus({
122787
+ state,
122788
+ version: info?.version,
122789
+ message: info?.message
122790
+ }, 4000);
122791
+ } else {
122792
+ this.statusBar.setUpdateStatus({
122793
+ state,
122794
+ version: info?.version,
122795
+ message: info?.message
122796
+ });
122797
+ }
122798
+ }
122799
+ });
122129
122800
  this.permissionQueue = new PermissionQueue({
122130
122801
  onShow: (item) => {
122131
122802
  if (this.activeModal)
@@ -122222,6 +122893,7 @@ class TUIApp {
122222
122893
  this.engine.mount(this.streamingView, { kind: "custom" });
122223
122894
  this.engine.mount(this.promptInput, { keepCursorVisible: true, kind: "input" });
122224
122895
  this.engine.mount(this.statusBar, { kind: "custom" });
122896
+ this.updateChecker.startBackgroundCheck(3000);
122225
122897
  this.engine.addInputListener((chunk) => {
122226
122898
  const action = parseKeyInput(chunk);
122227
122899
  if (this.voiceController.isActive) {
@@ -122657,6 +123329,7 @@ class TUIApp {
122657
123329
  this.permissionQueue.clear();
122658
123330
  this.session.shutdown().catch(() => {});
122659
123331
  this.voiceController.dispose();
123332
+ this.updateChecker.dispose();
122660
123333
  this.engine.cleanupSync();
122661
123334
  if (this.onExitCallback) {
122662
123335
  this.onExitCallback();