steward-cli 0.12.0 → 0.14.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 +1151 -659
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -52395,6 +52395,8 @@ function formatToolStatus(options) {
52395
52395
  let mainLine = `${bullet} ${dispName}`;
52396
52396
  if (truncatedArg) {
52397
52397
  mainLine += `${source_default.dim("(")}${source_default.dim(truncatedArg)}${source_default.dim(")")}`;
52398
+ } else {
52399
+ mainLine += `${source_default.dim("()")}`;
52398
52400
  }
52399
52401
  const lines = [mainLine];
52400
52402
  if (status === "completed" && options.toolOutput) {
@@ -70954,7 +70956,6 @@ class ToolCatalog {
70954
70956
  aiTools[name] = tool({
70955
70957
  description: def.description,
70956
70958
  inputSchema: def.parameters,
70957
- parameters: def.parameters,
70958
70959
  execute: async (args) => {
70959
70960
  return def.execute(args, context);
70960
70961
  }
@@ -70966,13 +70967,52 @@ class ToolCatalog {
70966
70967
  var defaultToolCatalog = new ToolCatalog;
70967
70968
 
70968
70969
  // src/tools/read-file/index.ts
70969
- import { existsSync as existsSync9, lstatSync as lstatSync3, readFileSync as readFileSync8 } from "node:fs";
70970
- import { isAbsolute as isAbsolute2, resolve as resolve5 } from "node:path";
70970
+ import { existsSync as existsSync9, lstatSync as lstatSync3, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
70971
+ import { isAbsolute as isAbsolute2, resolve as resolve5, sep as sep3 } from "node:path";
70971
70972
  var readFileInputSchema = object({
70972
70973
  path: string2().describe("The path of the file to read (relative to current working directory or absolute)."),
70973
70974
  offset: number2().int().min(1).optional().describe("The line number to start reading from (1-indexed). Defaults to 1."),
70974
70975
  limit: number2().int().min(1).max(2000).optional().describe("The maximum number of lines to read. Defaults to 200.")
70975
70976
  });
70977
+ function isPathInside2(targetPath, basePath) {
70978
+ const normTarget = resolve5(targetPath);
70979
+ const normBase = resolve5(basePath);
70980
+ if (normTarget === normBase) {
70981
+ return true;
70982
+ }
70983
+ const prefix = normBase.endsWith(sep3) ? normBase : `${normBase}${sep3}`;
70984
+ return normTarget.startsWith(prefix);
70985
+ }
70986
+ function resolveSafeReadPath(cwd, filePath) {
70987
+ if (!filePath || !filePath.trim()) {
70988
+ throw new Error("File path cannot be empty.");
70989
+ }
70990
+ const trimmed = filePath.trim();
70991
+ const resolved = isAbsolute2(trimmed) ? resolve5(trimmed) : resolve5(cwd, trimmed);
70992
+ if (!isPathInside2(resolved, cwd)) {
70993
+ throw new Error(`Access denied: path "${filePath}" resolves outside the trusted workspace root ("${cwd}").`);
70994
+ }
70995
+ if (!existsSync9(resolved)) {
70996
+ throw new Error(`File not found: ${filePath}`);
70997
+ }
70998
+ let realTarget;
70999
+ try {
71000
+ realTarget = realpathSync3(resolved);
71001
+ } catch {
71002
+ realTarget = resolved;
71003
+ }
71004
+ if (!isPathInside2(realTarget, cwd)) {
71005
+ throw new Error(`Access denied: symlink "${filePath}" points outside the trusted workspace root.`);
71006
+ }
71007
+ const stat = lstatSync3(resolved);
71008
+ if (stat.isDirectory()) {
71009
+ throw new Error(`Cannot read path because it is a directory: ${filePath}`);
71010
+ }
71011
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
71012
+ throw new Error(`Cannot read special or non-regular file: ${filePath}`);
71013
+ }
71014
+ return realTarget;
71015
+ }
70976
71016
  function isBinaryBuffer(buffer) {
70977
71017
  const checkLength = Math.min(buffer.length, 1024);
70978
71018
  for (let i = 0;i < checkLength; i++) {
@@ -70985,7 +71025,7 @@ function isBinaryBuffer(buffer) {
70985
71025
  var readFileTool = {
70986
71026
  name: "read_file",
70987
71027
  displayName: "Read",
70988
- description: "Reads the contents of a file with 1-indexed line numbers. Supports reading specific line ranges via offset and limit. Read is read-only and does not mutate or checkpoint.",
71028
+ description: "Reads the contents of a file. Supports reading specific line ranges via offset and limit. Read is read-only and does not mutate or checkpoint.",
70989
71029
  parameters: readFileInputSchema,
70990
71030
  confirmationPolicy: "never",
70991
71031
  summarize: (_args, result) => {
@@ -70993,10 +71033,7 @@ var readFileTool = {
70993
71033
  return `Read ${lines} line${lines === 1 ? "" : "s"}`;
70994
71034
  },
70995
71035
  execute: async (args, context) => {
70996
- const targetPath = isAbsolute2(args.path) ? args.path : resolve5(context.cwd, args.path);
70997
- if (!existsSync9(targetPath)) {
70998
- throw new Error(`File not found: ${args.path}`);
70999
- }
71036
+ const targetPath = resolveSafeReadPath(context.cwd, args.path);
71000
71037
  const stat = lstatSync3(targetPath);
71001
71038
  if (stat.isDirectory()) {
71002
71039
  throw new Error(`Cannot read path because it is a directory: ${args.path}`);
@@ -71032,18 +71069,13 @@ var readFileTool = {
71032
71069
  const startIndex = Math.max(0, startLine - 1);
71033
71070
  const endIndex = Math.min(totalLines, startIndex + limit);
71034
71071
  const slice = allLines.slice(startIndex, endIndex);
71035
- const padWidth = String(endIndex).length;
71036
- const formattedContent = slice.map((line, idx) => {
71037
- const lineNum = String(startIndex + idx + 1).padStart(padWidth, " ");
71038
- return `${lineNum} | ${line}`;
71039
- }).join(`
71040
- `);
71041
71072
  return {
71042
71073
  path: args.path,
71043
71074
  totalLines,
71044
71075
  startLine,
71045
71076
  endLine: endIndex,
71046
- content: formattedContent,
71077
+ content: slice.join(`
71078
+ `),
71047
71079
  isTruncated: endIndex < totalLines,
71048
71080
  linesRead: slice.length
71049
71081
  };
@@ -71051,6 +71083,9 @@ var readFileTool = {
71051
71083
  };
71052
71084
 
71053
71085
  // src/tools/write-file/index.ts
71086
+ import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3 } from "node:fs";
71087
+
71088
+ // src/utils/atomic-write.ts
71054
71089
  import { randomUUID as randomUUID5 } from "node:crypto";
71055
71090
  import {
71056
71091
  chmodSync as chmodSync2,
@@ -71059,13 +71094,46 @@ import {
71059
71094
  fsyncSync as fsyncSync5,
71060
71095
  mkdirSync as mkdirSync7,
71061
71096
  openSync as openSync6,
71062
- readFileSync as readFileSync9,
71063
71097
  renameSync as renameSync5,
71064
- statSync as statSync3,
71065
71098
  unlinkSync as unlinkSync5,
71066
71099
  writeSync as writeSync5
71067
71100
  } from "node:fs";
71068
71101
  import { dirname as dirname5, join as join7 } from "node:path";
71102
+ function atomicWriteFileSync(targetPath, content, options = {}) {
71103
+ const buffer = typeof content === "string" ? Buffer.from(content, options.encoding ?? "utf-8") : content;
71104
+ const dir = dirname5(targetPath);
71105
+ if (!existsSync10(dir)) {
71106
+ mkdirSync7(dir, { recursive: true });
71107
+ }
71108
+ const mode = options.mode ?? 420;
71109
+ const tmpPath = join7(dir, `.tmp-write-${randomUUID5().slice(0, 8)}`);
71110
+ let fd = null;
71111
+ try {
71112
+ fd = openSync6(tmpPath, "w", mode);
71113
+ writeSync5(fd, buffer, 0, buffer.length);
71114
+ fsyncSync5(fd);
71115
+ closeSync6(fd);
71116
+ fd = null;
71117
+ try {
71118
+ chmodSync2(tmpPath, mode);
71119
+ } catch {}
71120
+ renameSync5(tmpPath, targetPath);
71121
+ } catch (err) {
71122
+ if (fd !== null) {
71123
+ try {
71124
+ closeSync6(fd);
71125
+ } catch {}
71126
+ }
71127
+ if (existsSync10(tmpPath)) {
71128
+ try {
71129
+ unlinkSync5(tmpPath);
71130
+ } catch {}
71131
+ }
71132
+ throw err;
71133
+ }
71134
+ }
71135
+
71136
+ // src/tools/write-file/index.ts
71069
71137
  var writeFileInputSchema = object({
71070
71138
  file_path: string2().describe("The path of the file to write (relative to current working directory or absolute inside trusted workspace)."),
71071
71139
  content: string2().describe("The full text content to write to the file.")
@@ -71097,7 +71165,7 @@ ${detail}${overflow}`;
71097
71165
  },
71098
71166
  execute: async (args, context) => {
71099
71167
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
71100
- const isNew = !existsSync10(targetPath);
71168
+ const isNew = !existsSync11(targetPath);
71101
71169
  const beforeContent = isNew ? null : readFileSync9(targetPath, "utf-8");
71102
71170
  const afterContent = args.content;
71103
71171
  const linesWritten = afterContent.length === 0 ? 0 : afterContent.split(/\r?\n/).length;
@@ -71133,11 +71201,11 @@ ${detail}${overflow}`;
71133
71201
  throw new Error("File write permission denied by user.");
71134
71202
  }
71135
71203
  if (isNew) {
71136
- if (existsSync10(targetPath)) {
71204
+ if (existsSync11(targetPath)) {
71137
71205
  throw new Error(`File was created externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71138
71206
  }
71139
71207
  } else {
71140
- if (!existsSync10(targetPath)) {
71208
+ if (!existsSync11(targetPath)) {
71141
71209
  throw new Error(`File was deleted externally during approval review: "${relativePath}". Mutation aborted.`);
71142
71210
  }
71143
71211
  const freshContent = readFileSync9(targetPath, "utf-8");
@@ -71145,42 +71213,14 @@ ${detail}${overflow}`;
71145
71213
  throw new Error(`File was modified externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71146
71214
  }
71147
71215
  }
71148
- const newBuffer = Buffer.from(afterContent, "utf-8");
71149
- const dir = dirname5(targetPath);
71150
- if (!existsSync10(dir)) {
71151
- mkdirSync7(dir, { recursive: true });
71152
- }
71153
71216
  let existingMode = 420;
71154
71217
  if (!isNew) {
71155
71218
  try {
71156
71219
  existingMode = statSync3(targetPath).mode;
71157
71220
  } catch {}
71158
71221
  }
71159
- const tmpPath = join7(dir, `.tmp-write-${randomUUID5().slice(0, 8)}`);
71160
- let fd = null;
71161
- try {
71162
- fd = openSync6(tmpPath, "w", existingMode);
71163
- writeSync5(fd, newBuffer, 0, newBuffer.length);
71164
- fsyncSync5(fd);
71165
- closeSync6(fd);
71166
- fd = null;
71167
- try {
71168
- chmodSync2(tmpPath, existingMode);
71169
- } catch {}
71170
- renameSync5(tmpPath, targetPath);
71171
- } catch (err) {
71172
- if (fd !== null) {
71173
- try {
71174
- closeSync6(fd);
71175
- } catch {}
71176
- }
71177
- if (existsSync10(tmpPath)) {
71178
- try {
71179
- unlinkSync5(tmpPath);
71180
- } catch {}
71181
- }
71182
- throw err;
71183
- }
71222
+ const newBuffer = Buffer.from(afterContent, "utf-8");
71223
+ atomicWriteFileSync(targetPath, newBuffer, { mode: existingMode });
71184
71224
  if (context.checkpointTracker) {
71185
71225
  await context.checkpointTracker.completeMutation(targetPath);
71186
71226
  }
@@ -71200,20 +71240,7 @@ ${detail}${overflow}`;
71200
71240
  };
71201
71241
 
71202
71242
  // src/tools/edit-file/index.ts
71203
- import { randomUUID as randomUUID6 } from "node:crypto";
71204
- import {
71205
- chmodSync as chmodSync3,
71206
- closeSync as closeSync7,
71207
- existsSync as existsSync11,
71208
- fsyncSync as fsyncSync6,
71209
- openSync as openSync7,
71210
- readFileSync as readFileSync10,
71211
- renameSync as renameSync6,
71212
- statSync as statSync4,
71213
- unlinkSync as unlinkSync6,
71214
- writeSync as writeSync6
71215
- } from "node:fs";
71216
- import { dirname as dirname6, join as join8 } from "node:path";
71243
+ import { existsSync as existsSync12, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
71217
71244
 
71218
71245
  // src/utils/diff.ts
71219
71246
  function tokenizeLines(content) {
@@ -71523,7 +71550,7 @@ ${diffLines.join(`
71523
71550
  },
71524
71551
  execute: async (args, context) => {
71525
71552
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
71526
- if (!existsSync11(targetPath)) {
71553
+ if (!existsSync12(targetPath)) {
71527
71554
  throw new Error(`Cannot edit file because it does not exist: "${args.file_path}". Use write_file to create new files.`);
71528
71555
  }
71529
71556
  const currentContent = readFileSync10(targetPath, "utf-8");
@@ -71574,65 +71601,30 @@ ${diffLines.join(`
71574
71601
  if (!perm.allowed) {
71575
71602
  throw new Error("File edit permission denied by user.");
71576
71603
  }
71577
- if (!existsSync11(targetPath)) {
71604
+ if (!existsSync12(targetPath)) {
71578
71605
  throw new Error(`File was deleted externally during approval review: "${relativePath}". Mutation aborted.`);
71579
71606
  }
71580
71607
  const freshContent = readFileSync10(targetPath, "utf-8");
71581
71608
  if (freshContent !== currentContent) {
71582
71609
  throw new Error(`File was modified externally during approval review: "${relativePath}". Mutation aborted to prevent data loss.`);
71583
71610
  }
71584
- const beforeLines = currentContent.split(/\r?\n/);
71585
- const afterLines = updatedContent.split(/\r?\n/);
71586
- const lineDelta = afterLines.length - beforeLines.length;
71611
+ const diff = buildUnifiedDiff(currentContent, updatedContent, 0);
71587
71612
  let addedLines = 0;
71588
71613
  let removedLines = 0;
71589
- const oldLinesInMatch = args.old_string.split(/\r?\n/).length;
71590
- const newLinesInMatch = args.new_string.split(/\r?\n/).length;
71591
- const totalReplacedOccurrences = args.replace_all ? matchCount : 1;
71592
- if (oldLinesInMatch === newLinesInMatch && !args.old_string.includes(`
71593
- `) && !args.new_string.includes(`
71594
- `)) {
71595
- addedLines = totalReplacedOccurrences;
71596
- removedLines = totalReplacedOccurrences;
71597
- } else {
71598
- removedLines = (oldLinesInMatch - 1) * totalReplacedOccurrences + (lineDelta < 0 ? Math.abs(lineDelta) : 0);
71599
- addedLines = (newLinesInMatch - 1) * totalReplacedOccurrences + (lineDelta > 0 ? lineDelta : 0);
71600
- if (addedLines === 0 && removedLines === 0 && args.old_string !== args.new_string) {
71601
- addedLines = totalReplacedOccurrences;
71602
- removedLines = totalReplacedOccurrences;
71614
+ for (const hunk of diff.hunks) {
71615
+ for (const line of hunk.lines) {
71616
+ if (line.kind === "addition")
71617
+ addedLines++;
71618
+ if (line.kind === "deletion")
71619
+ removedLines++;
71603
71620
  }
71604
71621
  }
71605
- const newBuffer = Buffer.from(updatedContent, "utf-8");
71606
- const dir = dirname6(targetPath);
71607
71622
  let existingMode = 420;
71608
71623
  try {
71609
71624
  existingMode = statSync4(targetPath).mode;
71610
71625
  } catch {}
71611
- const tmpPath = join8(dir, `.tmp-edit-${randomUUID6().slice(0, 8)}`);
71612
- let fd = null;
71613
- try {
71614
- fd = openSync7(tmpPath, "w", existingMode);
71615
- writeSync6(fd, newBuffer, 0, newBuffer.length);
71616
- fsyncSync6(fd);
71617
- closeSync7(fd);
71618
- fd = null;
71619
- try {
71620
- chmodSync3(tmpPath, existingMode);
71621
- } catch {}
71622
- renameSync6(tmpPath, targetPath);
71623
- } catch (err) {
71624
- if (fd !== null) {
71625
- try {
71626
- closeSync7(fd);
71627
- } catch {}
71628
- }
71629
- if (existsSync11(tmpPath)) {
71630
- try {
71631
- unlinkSync6(tmpPath);
71632
- } catch {}
71633
- }
71634
- throw err;
71635
- }
71626
+ const newBuffer = Buffer.from(updatedContent, "utf-8");
71627
+ atomicWriteFileSync(targetPath, newBuffer, { mode: existingMode });
71636
71628
  if (context.checkpointTracker) {
71637
71629
  await context.checkpointTracker.completeMutation(targetPath);
71638
71630
  }
@@ -71653,7 +71645,7 @@ ${diffLines.join(`
71653
71645
 
71654
71646
  // src/tools/glob/index.ts
71655
71647
  import { readdirSync as readdirSync2 } from "node:fs";
71656
- import { isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve6 } from "node:path";
71648
+ import { isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve6 } from "node:path";
71657
71649
  var globInputSchema = object({
71658
71650
  pattern: string2().describe('Glob pattern to match files against (e.g. "**/*.ts", "src/*.json").'),
71659
71651
  path: string2().optional().describe("Starting directory path. Defaults to workspace root."),
@@ -71682,10 +71674,10 @@ function walkDir(dir, baseDir, matcher, results, limit) {
71682
71674
  break;
71683
71675
  if (entry.isDirectory()) {
71684
71676
  if (!IGNORED_DIRS.has(entry.name)) {
71685
- walkDir(join9(dir, entry.name), baseDir, matcher, results, limit);
71677
+ walkDir(join8(dir, entry.name), baseDir, matcher, results, limit);
71686
71678
  }
71687
71679
  } else if (entry.isFile()) {
71688
- const rel = relative2(baseDir, join9(dir, entry.name));
71680
+ const rel = relative2(baseDir, join8(dir, entry.name));
71689
71681
  if (matcher(rel, entry.name)) {
71690
71682
  results.push(rel);
71691
71683
  }
@@ -71705,7 +71697,13 @@ var globTool = {
71705
71697
  },
71706
71698
  execute: async (args, context) => {
71707
71699
  const startTime = Date.now();
71708
- const searchRoot = args.path ? isAbsolute3(args.path) ? args.path : resolve6(context.cwd, args.path) : context.cwd;
71700
+ const searchRoot = args.path ? isAbsolute3(args.path) ? resolve6(args.path) : resolve6(context.cwd, args.path) : context.cwd;
71701
+ const normCwd = resolve6(context.cwd);
71702
+ const normTarget = resolve6(searchRoot);
71703
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71704
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71705
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71706
+ }
71709
71707
  const limit = args.limit ?? 100;
71710
71708
  const pattern = args.pattern.trim();
71711
71709
  const regex = globToRegex(pattern);
@@ -71728,7 +71726,7 @@ var globTool = {
71728
71726
 
71729
71727
  // src/tools/grep/index.ts
71730
71728
  import { readFileSync as readFileSync11, readdirSync as readdirSync3 } from "node:fs";
71731
- import { isAbsolute as isAbsolute4, join as join10, relative as relative3, resolve as resolve7 } from "node:path";
71729
+ import { isAbsolute as isAbsolute4, join as join9, relative as relative3, resolve as resolve7 } from "node:path";
71732
71730
  var grepInputSchema = object({
71733
71731
  pattern: string2().describe("The regex or text pattern to search for across files."),
71734
71732
  path: string2().optional().describe("Target directory or file path to search. Defaults to current directory."),
@@ -71757,7 +71755,7 @@ function searchInDir(dir, baseDir, regex, fileFilter, results, filesWithMatches,
71757
71755
  for (const entry of entries) {
71758
71756
  if (results.length >= limit)
71759
71757
  break;
71760
- const fullPath = join10(dir, entry.name);
71758
+ const fullPath = join9(dir, entry.name);
71761
71759
  if (entry.isDirectory()) {
71762
71760
  if (!IGNORED_DIRS2.has(entry.name)) {
71763
71761
  searchInDir(fullPath, baseDir, regex, fileFilter, results, filesWithMatches, limit);
@@ -71803,7 +71801,13 @@ var grepTool = {
71803
71801
  },
71804
71802
  execute: async (args, context) => {
71805
71803
  const startTime = Date.now();
71806
- const searchRoot = args.path ? isAbsolute4(args.path) ? args.path : resolve7(context.cwd, args.path) : context.cwd;
71804
+ const searchRoot = args.path ? isAbsolute4(args.path) ? resolve7(args.path) : resolve7(context.cwd, args.path) : context.cwd;
71805
+ const normCwd = resolve7(context.cwd);
71806
+ const normTarget = resolve7(searchRoot);
71807
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71808
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71809
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71810
+ }
71807
71811
  const limit = args.limit ?? 100;
71808
71812
  const isCaseSensitive = args.case_sensitive ?? false;
71809
71813
  const flags = isCaseSensitive ? "" : "i";
@@ -71858,7 +71862,7 @@ var grepTool = {
71858
71862
 
71859
71863
  // src/tools/list-dir/index.ts
71860
71864
  import { readdirSync as readdirSync4, statSync as statSync5 } from "node:fs";
71861
- import { isAbsolute as isAbsolute5, join as join11, resolve as resolve8 } from "node:path";
71865
+ import { isAbsolute as isAbsolute5, join as join10, resolve as resolve8 } from "node:path";
71862
71866
  var listDirInputSchema = object({
71863
71867
  path: string2().optional().describe("Directory path to list. Defaults to current working directory."),
71864
71868
  recursive: boolean2().optional().describe("If true, lists files recursively (depth 2). Defaults to false.")
@@ -71875,7 +71879,13 @@ var listDirTool = {
71875
71879
  return `Listed ${files} file${files === 1 ? "" : "s"}, ${dirs} director${dirs === 1 ? "y" : "ies"}`;
71876
71880
  },
71877
71881
  execute: async (args, context) => {
71878
- const targetPath = args.path ? isAbsolute5(args.path) ? args.path : resolve8(context.cwd, args.path) : context.cwd;
71882
+ const targetPath = args.path ? isAbsolute5(args.path) ? resolve8(args.path) : resolve8(context.cwd, args.path) : context.cwd;
71883
+ const normCwd = resolve8(context.cwd);
71884
+ const normTarget = resolve8(targetPath);
71885
+ const prefix = normCwd.endsWith("/") ? normCwd : `${normCwd}/`;
71886
+ if (normTarget !== normCwd && !normTarget.startsWith(prefix)) {
71887
+ throw new Error(`Access denied: path "${args.path}" resolves outside the trusted workspace root ("${context.cwd}").`);
71888
+ }
71879
71889
  const rawEntries = readdirSync4(targetPath, { withFileTypes: true });
71880
71890
  const entries = [];
71881
71891
  let fileCount = 0;
@@ -71892,7 +71902,7 @@ var listDirTool = {
71892
71902
  type = "file";
71893
71903
  fileCount++;
71894
71904
  try {
71895
- sizeBytes = statSync5(join11(targetPath, entry.name)).size;
71905
+ sizeBytes = statSync5(join10(targetPath, entry.name)).size;
71896
71906
  } catch {}
71897
71907
  } else {
71898
71908
  fileCount++;
@@ -71960,7 +71970,7 @@ import { rmSync as rmSync3 } from "node:fs";
71960
71970
  // src/services/tasks/process.ts
71961
71971
  import { spawn } from "node:child_process";
71962
71972
  import { createWriteStream, mkdirSync as mkdirSync8 } from "node:fs";
71963
- import { join as join12 } from "node:path";
71973
+ import { join as join11 } from "node:path";
71964
71974
  import { tmpdir, homedir as homedir4 } from "node:os";
71965
71975
 
71966
71976
  // src/services/tasks/shell.ts
@@ -72008,16 +72018,16 @@ class ShellExecution {
72008
72018
  this.command = options.command;
72009
72019
  this.cwd = options.cwd;
72010
72020
  this.startedAt = new Date().toISOString();
72011
- const taskDir = join12(homedir4(), ".steward", "tasks", options.sessionId || "default");
72021
+ const taskDir = join11(homedir4(), ".steward", "tasks", options.sessionId || "default");
72012
72022
  try {
72013
72023
  mkdirSync8(taskDir, { recursive: true });
72014
72024
  } catch {
72015
- const fallbackDir = join12(tmpdir(), "steward-tasks");
72025
+ const fallbackDir = join11(tmpdir(), "steward-tasks");
72016
72026
  mkdirSync8(fallbackDir, { recursive: true });
72017
- this.outputPath = join12(fallbackDir, `${this.taskId}.log`);
72027
+ this.outputPath = join11(fallbackDir, `${this.taskId}.log`);
72018
72028
  return;
72019
72029
  }
72020
- this.outputPath = join12(taskDir, `${this.taskId}.log`);
72030
+ this.outputPath = join11(taskDir, `${this.taskId}.log`);
72021
72031
  }
72022
72032
  get status() {
72023
72033
  switch (this.state) {
@@ -72534,27 +72544,38 @@ async function evaluateBashPermission(req, context) {
72534
72544
 
72535
72545
  // src/tools/bash/index.ts
72536
72546
  var bashInputSchema = object({
72537
- command: string2().min(1).describe("The command to execute in the system shell."),
72538
- explanation: string2().min(1).max(240).describe("Brief sentence explaining why this command is needed (displayed in permission prompt).")
72547
+ command: string2({
72548
+ required_error: "command is required"
72549
+ }).min(1, "command cannot be empty").describe("The command to execute in the system shell."),
72550
+ explanation: string2({
72551
+ required_error: "An explanation is mandatory for running a bash command.",
72552
+ invalid_type_error: "explanation must be a string."
72553
+ }).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).")
72539
72554
  });
72540
72555
  var bashTool = {
72541
72556
  name: "bash",
72542
72557
  displayName: "Bash",
72543
- 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).",
72558
+ 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).",
72544
72559
  parameters: bashInputSchema,
72545
72560
  confirmationPolicy: "never",
72561
+ summarizeArgs: (args) => {
72562
+ return args.command ?? "";
72563
+ },
72546
72564
  summarize: (_args, result) => {
72547
72565
  if (result && "status" in result && result.status === "backgrounded") {
72548
72566
  return `Moved to background · task id: ${result.taskId}`;
72549
72567
  }
72550
- const code = result?.exitCode ?? 0;
72568
+ const code = result && "exitCode" in result ? result.exitCode : 0;
72551
72569
  return `Ran successfully · exit code: ${code}`;
72552
72570
  },
72553
72571
  execute: async (args, context) => {
72554
- const command = args.command.trim();
72555
- const explanation = args.explanation.trim();
72572
+ const command = typeof args?.command === "string" ? args.command.trim() : "";
72573
+ if (!command) {
72574
+ throw new Error("Command is required and cannot be empty.");
72575
+ }
72576
+ const explanation = typeof args?.explanation === "string" ? args.explanation.trim() : "";
72556
72577
  if (!explanation) {
72557
- throw new Error("An explanation is required for running a bash command.");
72578
+ throw new Error("An explanation is mandatory for running a bash command.");
72558
72579
  }
72559
72580
  const perm = await evaluateBashPermission({ command, explanation }, context);
72560
72581
  if (!perm.allowed) {
@@ -72676,10 +72697,34 @@ var taskKillTool = {
72676
72697
  return context.shellTasks.kill(args.task_id);
72677
72698
  }
72678
72699
  };
72700
+
72701
+ // src/tools/task-list/index.ts
72702
+ var taskListInputSchema = object({});
72703
+ var taskListTool = {
72704
+ name: "task_list",
72705
+ displayName: "Task List",
72706
+ description: "Lists all currently tracked background shell tasks along with their IDs, statuses, commands, and exit codes.",
72707
+ parameters: taskListInputSchema,
72708
+ confirmationPolicy: "never",
72709
+ summarize: (_args, result) => {
72710
+ const count = result?.total ?? 0;
72711
+ return `Listed ${count} background task${count === 1 ? "" : "s"}`;
72712
+ },
72713
+ execute: async (_args, context) => {
72714
+ if (!context.shellTasks) {
72715
+ throw new Error("No shell tasks manager available in execution context.");
72716
+ }
72717
+ const tasks = context.shellTasks.list();
72718
+ return {
72719
+ tasks,
72720
+ total: tasks.length
72721
+ };
72722
+ }
72723
+ };
72679
72724
  // package.json
72680
72725
  var package_default = {
72681
72726
  name: "steward-cli",
72682
- version: "0.12.0",
72727
+ version: "0.14.0",
72683
72728
  description: "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
72684
72729
  author: "sapirrior",
72685
72730
  license: "MIT",
@@ -72896,6 +72941,727 @@ var webSearchTool = {
72896
72941
  };
72897
72942
  }
72898
72943
  };
72944
+ // src/services/todos/store.ts
72945
+ import {
72946
+ existsSync as existsSync13,
72947
+ mkdirSync as mkdirSync9,
72948
+ readFileSync as readFileSync12,
72949
+ renameSync as renameSync6,
72950
+ unlinkSync as unlinkSync6,
72951
+ writeFileSync as writeFileSync3,
72952
+ openSync as openSync7,
72953
+ fsyncSync as fsyncSync6,
72954
+ closeSync as closeSync7
72955
+ } from "node:fs";
72956
+ import { join as join12 } from "node:path";
72957
+ import { randomUUID as randomUUID6 } from "node:crypto";
72958
+
72959
+ // src/config/env.ts
72960
+ var ALL_PROVIDER_NAMES = [
72961
+ "gemini",
72962
+ "anthropic",
72963
+ "openai",
72964
+ "xai",
72965
+ "mistral",
72966
+ "deepseek",
72967
+ "openrouter",
72968
+ "custom"
72969
+ ];
72970
+ function getEnvConfig() {
72971
+ const geminiApiKey = process.env["GEMINI_API_KEY"]?.trim() || process.env["GOOGLE_GENERATIVE_AI_API_KEY"]?.trim() || undefined;
72972
+ const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || undefined;
72973
+ const openaiApiKey = process.env["OPENAI_API_KEY"]?.trim() || undefined;
72974
+ const xaiApiKey = process.env["XAI_API_KEY"]?.trim() || undefined;
72975
+ const mistralApiKey = process.env["MISTRAL_API_KEY"]?.trim() || undefined;
72976
+ const deepseekApiKey = process.env["DEEPSEEK_API_KEY"]?.trim() || undefined;
72977
+ const openrouterApiKey = process.env["OPENROUTER_API_KEY"]?.trim() || undefined;
72978
+ const customApiKey = process.env["CUSTOM_API_KEY"]?.trim() || undefined;
72979
+ const customApiModelName = process.env["CUSTOM_API_MODEL_NAME"]?.trim() || undefined;
72980
+ const customApiUrl = process.env["CUSTOM_API_URL"]?.trim() || undefined;
72981
+ return {
72982
+ geminiApiKey,
72983
+ anthropicApiKey,
72984
+ openaiApiKey,
72985
+ xaiApiKey,
72986
+ mistralApiKey,
72987
+ deepseekApiKey,
72988
+ openrouterApiKey,
72989
+ custom: {
72990
+ apiKey: customApiKey,
72991
+ modelName: customApiModelName,
72992
+ baseURL: customApiUrl
72993
+ }
72994
+ };
72995
+ }
72996
+ function hasProviderConfig(provider, config = getEnvConfig()) {
72997
+ switch (provider) {
72998
+ case "gemini":
72999
+ return Boolean(config.geminiApiKey);
73000
+ case "anthropic":
73001
+ return Boolean(config.anthropicApiKey);
73002
+ case "openai":
73003
+ return Boolean(config.openaiApiKey);
73004
+ case "xai":
73005
+ return Boolean(config.xaiApiKey);
73006
+ case "mistral":
73007
+ return Boolean(config.mistralApiKey);
73008
+ case "deepseek":
73009
+ return Boolean(config.deepseekApiKey);
73010
+ case "openrouter":
73011
+ return Boolean(config.openrouterApiKey);
73012
+ case "custom":
73013
+ return Boolean(config.custom.baseURL && config.custom.modelName);
73014
+ }
73015
+ }
73016
+ function getAvailableProviders(config = getEnvConfig()) {
73017
+ const providers = [];
73018
+ if (hasProviderConfig("gemini", config))
73019
+ providers.push("gemini");
73020
+ if (hasProviderConfig("anthropic", config))
73021
+ providers.push("anthropic");
73022
+ if (hasProviderConfig("openai", config))
73023
+ providers.push("openai");
73024
+ if (hasProviderConfig("xai", config))
73025
+ providers.push("xai");
73026
+ if (hasProviderConfig("mistral", config))
73027
+ providers.push("mistral");
73028
+ if (hasProviderConfig("deepseek", config))
73029
+ providers.push("deepseek");
73030
+ if (hasProviderConfig("openrouter", config))
73031
+ providers.push("openrouter");
73032
+ if (hasProviderConfig("custom", config))
73033
+ providers.push("custom");
73034
+ return providers;
73035
+ }
73036
+ // src/services/todos/store.ts
73037
+ var VALID_STATUSES = [
73038
+ "pending",
73039
+ "in_progress",
73040
+ "completed",
73041
+ "cancelled",
73042
+ "blocked"
73043
+ ];
73044
+ var MIN_ITEMS = 2;
73045
+ var MAX_ITEMS = 10;
73046
+
73047
+ class SessionLockQueue {
73048
+ queues = new Map;
73049
+ async runExclusive(sessionId, task) {
73050
+ const prev = this.queues.get(sessionId) ?? Promise.resolve();
73051
+ let release;
73052
+ const current = new Promise((resolve) => {
73053
+ release = resolve;
73054
+ });
73055
+ this.queues.set(sessionId, prev.then(() => current, () => current));
73056
+ try {
73057
+ await prev;
73058
+ return await task();
73059
+ } finally {
73060
+ release();
73061
+ if (this.queues.get(sessionId) === current) {
73062
+ this.queues.delete(sessionId);
73063
+ }
73064
+ }
73065
+ }
73066
+ }
73067
+ var sessionLocks = new SessionLockQueue;
73068
+ function sanitizeSessionId(sessionId) {
73069
+ if (!sessionId || typeof sessionId !== "string") {
73070
+ throw new Error("Invalid sessionId: must be a non-empty string");
73071
+ }
73072
+ const clean = sessionId.trim();
73073
+ if (!clean || clean.includes("..") || clean.includes("/") || clean.includes("\\")) {
73074
+ throw new Error(`Invalid sessionId for path resolution: "${sessionId}"`);
73075
+ }
73076
+ return clean;
73077
+ }
73078
+ function getSessionTodosDir(sessionId) {
73079
+ const safeId = sanitizeSessionId(sessionId);
73080
+ return join12(getSettingsDir(), "todos", safeId);
73081
+ }
73082
+ function getSessionTodosFilePath(sessionId) {
73083
+ return join12(getSessionTodosDir(sessionId), "todos.json");
73084
+ }
73085
+ function validateTodoList(todos) {
73086
+ if (!Array.isArray(todos)) {
73087
+ throw new Error("Invalid todos format: expected an array of todo items");
73088
+ }
73089
+ if (todos.length < MIN_ITEMS || todos.length > MAX_ITEMS) {
73090
+ throw new Error(`Todo list must contain between ${MIN_ITEMS} and ${MAX_ITEMS} items (received ${todos.length})`);
73091
+ }
73092
+ const seenIds = new Set;
73093
+ let inProgressCount = 0;
73094
+ const validated = [];
73095
+ for (let i = 0;i < todos.length; i++) {
73096
+ const item = todos[i];
73097
+ if (!item || typeof item !== "object") {
73098
+ throw new Error(`Invalid todo at index ${i}: item must be an object`);
73099
+ }
73100
+ const { id, description, status } = item;
73101
+ if (typeof id !== "string" || !id.trim()) {
73102
+ throw new Error(`Invalid todo at index ${i}: id must be a non-empty string`);
73103
+ }
73104
+ const trimmedId = id.trim();
73105
+ if (seenIds.has(trimmedId)) {
73106
+ throw new Error(`Duplicate todo id "${trimmedId}" at index ${i}`);
73107
+ }
73108
+ seenIds.add(trimmedId);
73109
+ if (typeof description !== "string" || !description.trim()) {
73110
+ throw new Error(`Invalid todo at index ${i} (${trimmedId}): description must be a non-empty string`);
73111
+ }
73112
+ if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
73113
+ throw new Error(`Invalid todo at index ${i} (${trimmedId}): status must be one of ${VALID_STATUSES.join(", ")} (got "${status}")`);
73114
+ }
73115
+ if (status === "in_progress") {
73116
+ inProgressCount++;
73117
+ }
73118
+ validated.push({
73119
+ id: trimmedId,
73120
+ description: description.trim(),
73121
+ status
73122
+ });
73123
+ }
73124
+ if (inProgressCount > 1) {
73125
+ throw new Error(`At most 1 todo item can be 'in_progress' at any time (found ${inProgressCount} in_progress items)`);
73126
+ }
73127
+ return validated;
73128
+ }
73129
+ async function readTodoList(sessionId) {
73130
+ const filePath = getSessionTodosFilePath(sessionId);
73131
+ if (!existsSync13(filePath)) {
73132
+ return null;
73133
+ }
73134
+ const raw = readFileSync12(filePath, "utf-8");
73135
+ let parsed;
73136
+ try {
73137
+ parsed = JSON.parse(raw);
73138
+ } catch (err) {
73139
+ throw new Error(`Corrupted todo state for session "${sessionId}": failed to parse JSON (${err instanceof Error ? err.message : String(err)})`);
73140
+ }
73141
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
73142
+ throw new Error(`Corrupted todo state for session "${sessionId}": root must be an object`);
73143
+ }
73144
+ const record = parsed;
73145
+ if (record.schemaVersion !== 1) {
73146
+ throw new Error(`Unsupported todo schemaVersion "${record.schemaVersion}" (expected 1) for session "${sessionId}"`);
73147
+ }
73148
+ if (typeof record.sessionId !== "string") {
73149
+ throw new Error(`Invalid todo state for session "${sessionId}": missing or invalid sessionId`);
73150
+ }
73151
+ const validatedTodos = validateTodoList(record.todos);
73152
+ return {
73153
+ schemaVersion: 1,
73154
+ sessionId: record.sessionId,
73155
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString(),
73156
+ todos: validatedTodos
73157
+ };
73158
+ }
73159
+ async function writeTodoList(sessionId, todos) {
73160
+ return sessionLocks.runExclusive(sessionId, async () => {
73161
+ const validated = validateTodoList(todos);
73162
+ const safeSessionId = sanitizeSessionId(sessionId);
73163
+ const dir = getSessionTodosDir(safeSessionId);
73164
+ if (!existsSync13(dir)) {
73165
+ mkdirSync9(dir, { recursive: true, mode: 448 });
73166
+ }
73167
+ const payload = {
73168
+ schemaVersion: 1,
73169
+ sessionId: safeSessionId,
73170
+ updatedAt: new Date().toISOString(),
73171
+ todos: validated
73172
+ };
73173
+ const targetPath = getSessionTodosFilePath(safeSessionId);
73174
+ const tempPath = join12(dir, `.todos.${randomUUID6()}.tmp`);
73175
+ const jsonStr = JSON.stringify(payload, null, 2) + `
73176
+ `;
73177
+ try {
73178
+ writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73179
+ const fd = openSync7(tempPath, "r");
73180
+ try {
73181
+ fsyncSync6(fd);
73182
+ } finally {
73183
+ closeSync7(fd);
73184
+ }
73185
+ renameSync6(tempPath, targetPath);
73186
+ } catch (err) {
73187
+ try {
73188
+ if (existsSync13(tempPath)) {
73189
+ unlinkSync6(tempPath);
73190
+ }
73191
+ } catch {}
73192
+ throw err;
73193
+ }
73194
+ return payload;
73195
+ });
73196
+ }
73197
+ async function updateTodoItem(sessionId, todoId, updates) {
73198
+ return sessionLocks.runExclusive(sessionId, async () => {
73199
+ if (!todoId || typeof todoId !== "string" || !todoId.trim()) {
73200
+ throw new Error("todoId must be a non-empty string");
73201
+ }
73202
+ const targetId = todoId.trim();
73203
+ const current = await readTodoList(sessionId);
73204
+ if (!current) {
73205
+ throw new Error(`No todo list found for session "${sessionId}". Create one first with TodoWrite.`);
73206
+ }
73207
+ const itemIndex = current.todos.findIndex((t) => t.id === targetId);
73208
+ if (itemIndex === -1) {
73209
+ throw new Error(`Todo item with id "${targetId}" not found in current session todo list`);
73210
+ }
73211
+ if (updates.status === undefined && updates.description === undefined) {
73212
+ throw new Error("At least one of status or description must be provided for update");
73213
+ }
73214
+ const currentItem = current.todos[itemIndex];
73215
+ const newStatus = updates.status !== undefined ? updates.status : currentItem.status;
73216
+ const newDescription = updates.description !== undefined ? updates.description.trim() : currentItem.description;
73217
+ if (!newDescription) {
73218
+ throw new Error("Updated description cannot be empty");
73219
+ }
73220
+ const updatedTodos = current.todos.map((item, idx) => {
73221
+ if (idx !== itemIndex) {
73222
+ return item;
73223
+ }
73224
+ return {
73225
+ ...item,
73226
+ status: newStatus,
73227
+ description: newDescription
73228
+ };
73229
+ });
73230
+ const validated = validateTodoList(updatedTodos);
73231
+ const safeSessionId = sanitizeSessionId(sessionId);
73232
+ const dir = getSessionTodosDir(safeSessionId);
73233
+ if (!existsSync13(dir)) {
73234
+ mkdirSync9(dir, { recursive: true, mode: 448 });
73235
+ }
73236
+ const payload = {
73237
+ schemaVersion: 1,
73238
+ sessionId: safeSessionId,
73239
+ updatedAt: new Date().toISOString(),
73240
+ todos: validated
73241
+ };
73242
+ const targetPath = getSessionTodosFilePath(safeSessionId);
73243
+ const tempPath = join12(dir, `.todos.${randomUUID6()}.tmp`);
73244
+ const jsonStr = JSON.stringify(payload, null, 2) + `
73245
+ `;
73246
+ try {
73247
+ writeFileSync3(tempPath, jsonStr, { encoding: "utf-8", mode: 384 });
73248
+ const fd = openSync7(tempPath, "r");
73249
+ try {
73250
+ fsyncSync6(fd);
73251
+ } finally {
73252
+ closeSync7(fd);
73253
+ }
73254
+ renameSync6(tempPath, targetPath);
73255
+ } catch (err) {
73256
+ try {
73257
+ if (existsSync13(tempPath)) {
73258
+ unlinkSync6(tempPath);
73259
+ }
73260
+ } catch {}
73261
+ throw err;
73262
+ }
73263
+ return payload;
73264
+ });
73265
+ }
73266
+ // src/tools/todo-write/index.ts
73267
+ var todoItemSchema = object({
73268
+ id: string2().min(1, "id is required").describe("Unique identifier for the todo item"),
73269
+ description: string2().min(1, "description is required").describe("Clear description of the task"),
73270
+ status: _enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).describe("Current status of the todo item")
73271
+ });
73272
+ var todoWriteParamsSchema = object({
73273
+ 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")
73274
+ });
73275
+ function formatTodoCheckbox(status) {
73276
+ switch (status) {
73277
+ case "completed":
73278
+ return "[x]";
73279
+ case "in_progress":
73280
+ return `[${figures.sliderPointer}]`;
73281
+ case "cancelled":
73282
+ return `[${figures.cross}]`;
73283
+ case "blocked":
73284
+ return `[${figures.warning}]`;
73285
+ case "pending":
73286
+ default:
73287
+ return "[ ]";
73288
+ }
73289
+ }
73290
+ function formatTodoListSummary(todos, maxItems = 10) {
73291
+ const visible = todos.slice(0, maxItems);
73292
+ const hiddenCount = todos.length - visible.length;
73293
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox(t.status)} ${t.description}`);
73294
+ if (hiddenCount > 0) {
73295
+ lines.push(`… and ${hiddenCount} more`);
73296
+ }
73297
+ return lines.join(`
73298
+ `);
73299
+ }
73300
+ var todoWriteTool = {
73301
+ name: "todo_write",
73302
+ displayName: "TodoWrite",
73303
+ icon: "\uD83D\uDCDD",
73304
+ 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.",
73305
+ parameters: todoWriteParamsSchema,
73306
+ summarizeArgs(args) {
73307
+ if (!args.todos || args.todos.length === 0)
73308
+ return "";
73309
+ if (args.todos.length <= 2) {
73310
+ return args.todos.map((t) => `"${t.description}"`).join(", ");
73311
+ }
73312
+ return `"${args.todos[0]?.description}", "${args.todos[1]?.description}", +${args.todos.length - 2} more`;
73313
+ },
73314
+ async execute(args, context) {
73315
+ if (!context.sessionId) {
73316
+ throw new Error("TodoWrite requires an active session ID in ToolContext");
73317
+ }
73318
+ return writeTodoList(context.sessionId, args.todos);
73319
+ },
73320
+ summarize(args, result) {
73321
+ const list = result?.todos ?? args.todos;
73322
+ const total = list.length;
73323
+ const header = `Added ${total} todos`;
73324
+ const details = formatTodoListSummary(list);
73325
+ return `${header}
73326
+ ${details}`;
73327
+ }
73328
+ };
73329
+
73330
+ // src/tools/todo-update/index.ts
73331
+ var todoUpdateParamsSchema = object({
73332
+ id: string2().min(1, "id is required").describe("ID of the existing todo item to update"),
73333
+ status: _enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).optional().describe("New status for the todo item"),
73334
+ description: string2().min(1).optional().describe("Updated description for the todo item")
73335
+ });
73336
+ function formatTodoCheckbox2(status) {
73337
+ switch (status) {
73338
+ case "completed":
73339
+ return "[x]";
73340
+ case "in_progress":
73341
+ return `[${figures.sliderPointer}]`;
73342
+ case "cancelled":
73343
+ return `[${figures.cross}]`;
73344
+ case "blocked":
73345
+ return `[${figures.warning}]`;
73346
+ case "pending":
73347
+ default:
73348
+ return "[ ]";
73349
+ }
73350
+ }
73351
+ function formatTodoListSummary2(todos, maxItems = 10) {
73352
+ const visible = todos.slice(0, maxItems);
73353
+ const hiddenCount = todos.length - visible.length;
73354
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox2(t.status)} ${t.description}`);
73355
+ if (hiddenCount > 0) {
73356
+ lines.push(`… and ${hiddenCount} more`);
73357
+ }
73358
+ return lines.join(`
73359
+ `);
73360
+ }
73361
+ var todoUpdateTool = {
73362
+ name: "todo_update",
73363
+ displayName: "TodoUpdate",
73364
+ icon: "✓",
73365
+ description: "Updates a single todo item status or description in the current session. Cannot create new items or update multiple items at once.",
73366
+ parameters: todoUpdateParamsSchema,
73367
+ summarizeArgs(args) {
73368
+ const parts = [`id: "${args.id}"`];
73369
+ if (args.status)
73370
+ parts.push(`status: "${args.status}"`);
73371
+ if (args.description)
73372
+ parts.push(`desc: "${args.description}"`);
73373
+ return parts.join(", ");
73374
+ },
73375
+ async execute(args, context) {
73376
+ if (!context.sessionId) {
73377
+ throw new Error("TodoUpdate requires an active session ID in ToolContext");
73378
+ }
73379
+ return updateTodoItem(context.sessionId, args.id, {
73380
+ status: args.status,
73381
+ description: args.description
73382
+ });
73383
+ },
73384
+ summarize(args, result) {
73385
+ if (!result || !result.todos) {
73386
+ return `Todo ${args.id} updated`;
73387
+ }
73388
+ const total = result.todos.length;
73389
+ const completed = result.todos.filter((t) => t.status === "completed").length;
73390
+ const updatedItem = result.todos.find((t) => t.id === args.id);
73391
+ const statusText = updatedItem?.status ?? args.status ?? "updated";
73392
+ const header = `Todo ${args.id} ${statusText} · ${completed}/${total} done`;
73393
+ const details = formatTodoListSummary2(result.todos);
73394
+ return `${header}
73395
+ ${details}`;
73396
+ }
73397
+ };
73398
+
73399
+ // src/tools/todo-read/index.ts
73400
+ var todoReadParamsSchema = object({});
73401
+ function formatTodoCheckbox3(status) {
73402
+ switch (status) {
73403
+ case "completed":
73404
+ return "[x]";
73405
+ case "in_progress":
73406
+ return `[${figures.sliderPointer}]`;
73407
+ case "cancelled":
73408
+ return `[${figures.cross}]`;
73409
+ case "blocked":
73410
+ return `[${figures.warning}]`;
73411
+ case "pending":
73412
+ default:
73413
+ return "[ ]";
73414
+ }
73415
+ }
73416
+ function formatTodoListSummary3(todos, maxItems = 10) {
73417
+ const visible = todos.slice(0, maxItems);
73418
+ const hiddenCount = todos.length - visible.length;
73419
+ const lines = visible.map((t, i) => `${i + 1}. ${formatTodoCheckbox3(t.status)} ${t.description}`);
73420
+ if (hiddenCount > 0) {
73421
+ lines.push(`… and ${hiddenCount} more`);
73422
+ }
73423
+ return lines.join(`
73424
+ `);
73425
+ }
73426
+ var todoReadTool = {
73427
+ name: "todo_read",
73428
+ displayName: "TodoRead",
73429
+ icon: "\uD83D\uDCCB",
73430
+ description: "Reads the current session todo list without modifying it. Returns an empty result if no todos have been established yet.",
73431
+ parameters: todoReadParamsSchema,
73432
+ summarizeArgs() {
73433
+ return "";
73434
+ },
73435
+ async execute(_args, context) {
73436
+ if (!context.sessionId) {
73437
+ throw new Error("TodoRead requires an active session ID in ToolContext");
73438
+ }
73439
+ const list = await readTodoList(context.sessionId);
73440
+ if (!list) {
73441
+ return {
73442
+ hasTodos: false,
73443
+ todos: [],
73444
+ message: "No todos currently established for this session."
73445
+ };
73446
+ }
73447
+ return {
73448
+ hasTodos: true,
73449
+ todos: list.todos,
73450
+ updatedAt: list.updatedAt
73451
+ };
73452
+ },
73453
+ summarize(_args, result) {
73454
+ if (!result || !result.hasTodos || result.todos.length === 0) {
73455
+ return "No active todos";
73456
+ }
73457
+ const total = result.todos.length;
73458
+ const completed = result.todos.filter((t) => t.status === "completed").length;
73459
+ const active = result.todos.find((t) => t.status === "in_progress");
73460
+ const header = active ? `Todos ${completed}/${total} done · ▶ ${active.description}` : `Todos ${completed}/${total} done`;
73461
+ const details = formatTodoListSummary3(result.todos);
73462
+ return `${header}
73463
+ ${details}`;
73464
+ }
73465
+ };
73466
+ // src/skills/discovery.ts
73467
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6 } from "node:fs";
73468
+ import { homedir as homedir5 } from "node:os";
73469
+ import { isAbsolute as isAbsolute6, join as join13, normalize, resolve as resolve9, sep as sep4 } from "node:path";
73470
+ var MAX_SKILL_FILE_SIZE = 64 * 1024;
73471
+ function parseSkillFrontmatter(content) {
73472
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
73473
+ if (!match || !match[1]) {
73474
+ return null;
73475
+ }
73476
+ const rawYaml = match[1];
73477
+ const lines = rawYaml.split(/\r?\n/);
73478
+ const result = {};
73479
+ let currentKey = null;
73480
+ let descriptionLines = [];
73481
+ for (const line of lines) {
73482
+ const trimmed = line.trim();
73483
+ if (!trimmed || trimmed.startsWith("#"))
73484
+ continue;
73485
+ const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
73486
+ if (keyMatch && keyMatch[1]) {
73487
+ const key = keyMatch[1].toLowerCase();
73488
+ const val = (keyMatch[2] ?? "").trim();
73489
+ if (key === "name") {
73490
+ currentKey = "name";
73491
+ result.name = unquote(val);
73492
+ } else if (key === "description") {
73493
+ currentKey = "description";
73494
+ if (val === ">" || val === ">-" || val === "|" || val === "|-") {
73495
+ descriptionLines = [];
73496
+ } else if (val) {
73497
+ descriptionLines = [unquote(val)];
73498
+ } else {
73499
+ descriptionLines = [];
73500
+ }
73501
+ } else {
73502
+ currentKey = null;
73503
+ }
73504
+ } else if (currentKey === "description" && (line.startsWith(" ") || line.startsWith("\t"))) {
73505
+ descriptionLines.push(trimmed);
73506
+ }
73507
+ }
73508
+ if (descriptionLines.length > 0) {
73509
+ result.description = descriptionLines.join(" ").trim();
73510
+ }
73511
+ return result;
73512
+ }
73513
+ function unquote(str) {
73514
+ const trimmed = str.trim();
73515
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
73516
+ return trimmed.slice(1, -1);
73517
+ }
73518
+ return trimmed;
73519
+ }
73520
+ function discoverSkills(cwd = process.cwd()) {
73521
+ const targets = [
73522
+ { dir: join13(cwd, ".agents", "skills"), source: "workspace" },
73523
+ { dir: join13(homedir5(), ".agents", "skills"), source: "user-agents" },
73524
+ { dir: join13(homedir5(), ".steward", "skills"), source: "user-steward" }
73525
+ ];
73526
+ const skillMap = new Map;
73527
+ for (const target of targets) {
73528
+ if (!existsSync14(target.dir)) {
73529
+ continue;
73530
+ }
73531
+ try {
73532
+ const entries = readdirSync5(target.dir, { withFileTypes: true });
73533
+ for (const entry of entries) {
73534
+ if (!entry.isDirectory())
73535
+ continue;
73536
+ const skillDir = join13(target.dir, entry.name);
73537
+ const skillMdPath = join13(skillDir, "SKILL.md");
73538
+ const fallbackMdPath = join13(skillDir, "skill.md");
73539
+ const resolvedFile = existsSync14(skillMdPath) ? skillMdPath : existsSync14(fallbackMdPath) ? fallbackMdPath : null;
73540
+ if (!resolvedFile)
73541
+ continue;
73542
+ try {
73543
+ const raw = readFileSync13(resolvedFile, "utf-8");
73544
+ const frontmatter = parseSkillFrontmatter(raw);
73545
+ const skillName = frontmatter?.name || entry.name;
73546
+ const description = frontmatter?.description || "No description provided.";
73547
+ if (!skillMap.has(skillName)) {
73548
+ skillMap.set(skillName, {
73549
+ name: skillName,
73550
+ description,
73551
+ filePath: resolvedFile,
73552
+ dirPath: skillDir,
73553
+ source: target.source
73554
+ });
73555
+ }
73556
+ } catch {}
73557
+ }
73558
+ } catch {}
73559
+ }
73560
+ return Array.from(skillMap.values());
73561
+ }
73562
+ function getSkill(name, cwd = process.cwd()) {
73563
+ const cleanName = name.trim();
73564
+ const all = discoverSkills(cwd);
73565
+ return all.find((s) => s.name.toLowerCase() === cleanName.toLowerCase()) ?? null;
73566
+ }
73567
+ function readSkillResource(name, relativePath, cwd = process.cwd(), maxBytes = MAX_SKILL_FILE_SIZE) {
73568
+ const skill = getSkill(name, cwd);
73569
+ if (!skill) {
73570
+ throw new Error(`Skill "${name}" not found`);
73571
+ }
73572
+ let targetFilePath;
73573
+ if (!relativePath || !relativePath.trim()) {
73574
+ targetFilePath = skill.filePath;
73575
+ } else {
73576
+ const trimmedPath = relativePath.trim();
73577
+ if (isAbsolute6(trimmedPath)) {
73578
+ throw new Error(`Invalid skill resource path: absolute paths are not permitted (${trimmedPath})`);
73579
+ }
73580
+ const normalizedRelative = normalize(trimmedPath);
73581
+ if (normalizedRelative.startsWith("..") || normalizedRelative.includes(`${sep4}..`)) {
73582
+ throw new Error(`Invalid skill resource path: directory traversal ("..") is forbidden (${trimmedPath})`);
73583
+ }
73584
+ targetFilePath = resolve9(skill.dirPath, normalizedRelative);
73585
+ const resolvedDir = resolve9(skill.dirPath);
73586
+ if (!targetFilePath.startsWith(resolvedDir + sep4) && targetFilePath !== resolvedDir) {
73587
+ throw new Error(`Resource "${trimmedPath}" resolves outside skill directory`);
73588
+ }
73589
+ }
73590
+ if (!existsSync14(targetFilePath)) {
73591
+ throw new Error(`Skill resource file "${targetFilePath}" does not exist`);
73592
+ }
73593
+ const stat = statSync6(targetFilePath);
73594
+ if (!stat.isFile()) {
73595
+ throw new Error(`Skill resource path "${targetFilePath}" is not a regular file`);
73596
+ }
73597
+ if (stat.size > maxBytes) {
73598
+ throw new Error(`Skill resource file exceeds size limit of ${maxBytes} bytes (file is ${stat.size} bytes)`);
73599
+ }
73600
+ const content = readFileSync13(targetFilePath, "utf-8");
73601
+ return {
73602
+ name: skill.name,
73603
+ description: skill.description,
73604
+ source: skill.source,
73605
+ resourcePath: targetFilePath,
73606
+ content
73607
+ };
73608
+ }
73609
+ // src/tools/skill-list/index.ts
73610
+ var skillListParamsSchema = object({});
73611
+ var skillListTool = {
73612
+ name: "skill_list",
73613
+ displayName: "SkillList",
73614
+ icon: "\uD83D\uDCA1",
73615
+ description: "Discovers and lists all available specialized skills and their descriptions. Use this when you need specialized domain knowledge or workflows.",
73616
+ parameters: skillListParamsSchema,
73617
+ summarizeArgs() {
73618
+ return "";
73619
+ },
73620
+ async execute(_args, context) {
73621
+ const cwd = context?.cwd ?? process.cwd();
73622
+ const skills = discoverSkills(cwd);
73623
+ return skills.map((s) => ({
73624
+ name: s.name,
73625
+ description: s.description,
73626
+ source: s.source
73627
+ })).sort((a, b) => a.name.localeCompare(b.name));
73628
+ },
73629
+ summarize(_args, result) {
73630
+ const count = result?.length ?? 0;
73631
+ if (count === 0) {
73632
+ return "Discovered 0 skills";
73633
+ }
73634
+ return `Discovered ${count} skill${count === 1 ? "" : "s"}`;
73635
+ }
73636
+ };
73637
+
73638
+ // src/tools/skill-read/index.ts
73639
+ var skillReadParamsSchema = object({
73640
+ name: string2().min(1, "Skill name is required"),
73641
+ path: string2().optional()
73642
+ });
73643
+ var skillReadTool = {
73644
+ name: "skill_read",
73645
+ displayName: "SkillRead",
73646
+ icon: "\uD83D\uDCD6",
73647
+ 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.",
73648
+ parameters: skillReadParamsSchema,
73649
+ summarizeArgs(args) {
73650
+ if (args.path) {
73651
+ return `name="${args.name}" path="${args.path}"`;
73652
+ }
73653
+ return `name="${args.name}"`;
73654
+ },
73655
+ async execute(args, context) {
73656
+ const cwd = context?.cwd ?? process.cwd();
73657
+ return readSkillResource(args.name, args.path, cwd);
73658
+ },
73659
+ summarize(args, result) {
73660
+ const target = args.path ? `${args.name}/${args.path}` : args.name;
73661
+ const len = result?.content ? ` (${result.content.length} bytes)` : "";
73662
+ return `SkillRead: ${target}${len}`;
73663
+ }
73664
+ };
72899
73665
 
72900
73666
  // src/tools/index.ts
72901
73667
  var builtInTools = [
@@ -72907,11 +73673,17 @@ var builtInTools = [
72907
73673
  listDirTool,
72908
73674
  sleepTool,
72909
73675
  bashTool,
73676
+ taskListTool,
72910
73677
  taskReadTool,
72911
73678
  taskSendInputTool,
72912
73679
  taskKillTool,
72913
73680
  webFetchTool,
72914
- webSearchTool
73681
+ webSearchTool,
73682
+ todoWriteTool,
73683
+ todoUpdateTool,
73684
+ todoReadTool,
73685
+ skillListTool,
73686
+ skillReadTool
72915
73687
  ];
72916
73688
  for (const tool of builtInTools) {
72917
73689
  defaultToolCatalog.register(tool);
@@ -72953,6 +73725,7 @@ var MAX_RETAINED_TERMINAL_TASKS = 50;
72953
73725
  class ShellTaskManager {
72954
73726
  tasks = new Map;
72955
73727
  terminalTimestamps = new Map;
73728
+ evictedTaskReasons = new Map;
72956
73729
  isShuttingDown = false;
72957
73730
  generateTaskId() {
72958
73731
  return `task-${randomBytes(4).toString("hex")}`;
@@ -72986,7 +73759,7 @@ class ShellTaskManager {
72986
73759
  this.evictExpiredTasks();
72987
73760
  const exec = this.tasks.get(taskId);
72988
73761
  if (!exec) {
72989
- throw new Error(`Task "${taskId}" not found or expired.`);
73762
+ throw this.getTaskNotFoundError(taskId);
72990
73763
  }
72991
73764
  const rawOutput = exec.getRecentOutput();
72992
73765
  const { preview, truncated } = boundResultText(rawOutput);
@@ -73007,7 +73780,7 @@ class ShellTaskManager {
73007
73780
  this.evictExpiredTasks();
73008
73781
  const exec = this.tasks.get(taskId);
73009
73782
  if (!exec) {
73010
- throw new Error(`Task "${taskId}" not found or expired.`);
73783
+ throw this.getTaskNotFoundError(taskId);
73011
73784
  }
73012
73785
  if (!exec.isRunning) {
73013
73786
  throw new Error(`Task "${taskId}" is not running (status: ${exec.status}).`);
@@ -73028,7 +73801,7 @@ class ShellTaskManager {
73028
73801
  this.evictExpiredTasks();
73029
73802
  const exec = this.tasks.get(taskId);
73030
73803
  if (!exec) {
73031
- throw new Error(`Task "${taskId}" not found or expired.`);
73804
+ throw this.getTaskNotFoundError(taskId);
73032
73805
  }
73033
73806
  if (exec.isRunning) {
73034
73807
  await exec.kill();
@@ -73102,6 +73875,7 @@ class ShellTaskManager {
73102
73875
  }
73103
73876
  this.tasks.delete(id);
73104
73877
  this.terminalTimestamps.delete(id);
73878
+ this.recordEviction(id, "expired due to 10-minute retention TTL");
73105
73879
  }
73106
73880
  }
73107
73881
  const remainingTerminal = Array.from(this.terminalTimestamps.entries()).sort((a, b) => a[1] - b[1]);
@@ -73115,86 +73889,25 @@ class ShellTaskManager {
73115
73889
  }
73116
73890
  this.tasks.delete(oldestId);
73117
73891
  this.terminalTimestamps.delete(oldestId);
73892
+ this.recordEviction(oldestId, "evicted due to task capacity limit (50 tasks max)");
73118
73893
  }
73119
73894
  }
73120
- }
73121
-
73122
- // src/config/env.ts
73123
- var ALL_PROVIDER_NAMES = [
73124
- "gemini",
73125
- "anthropic",
73126
- "openai",
73127
- "xai",
73128
- "mistral",
73129
- "deepseek",
73130
- "openrouter",
73131
- "custom"
73132
- ];
73133
- function getEnvConfig() {
73134
- const geminiApiKey = process.env["GEMINI_API_KEY"]?.trim() || process.env["GOOGLE_GENERATIVE_AI_API_KEY"]?.trim() || undefined;
73135
- const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || undefined;
73136
- const openaiApiKey = process.env["OPENAI_API_KEY"]?.trim() || undefined;
73137
- const xaiApiKey = process.env["XAI_API_KEY"]?.trim() || undefined;
73138
- const mistralApiKey = process.env["MISTRAL_API_KEY"]?.trim() || undefined;
73139
- const deepseekApiKey = process.env["DEEPSEEK_API_KEY"]?.trim() || undefined;
73140
- const openrouterApiKey = process.env["OPENROUTER_API_KEY"]?.trim() || undefined;
73141
- const customApiKey = process.env["CUSTOM_API_KEY"]?.trim() || undefined;
73142
- const customApiModelName = process.env["CUSTOM_API_MODEL_NAME"]?.trim() || undefined;
73143
- const customApiUrl = process.env["CUSTOM_API_URL"]?.trim() || undefined;
73144
- return {
73145
- geminiApiKey,
73146
- anthropicApiKey,
73147
- openaiApiKey,
73148
- xaiApiKey,
73149
- mistralApiKey,
73150
- deepseekApiKey,
73151
- openrouterApiKey,
73152
- custom: {
73153
- apiKey: customApiKey,
73154
- modelName: customApiModelName,
73155
- baseURL: customApiUrl
73895
+ recordEviction(taskId, reason) {
73896
+ if (this.evictedTaskReasons.size >= 100) {
73897
+ const oldest = this.evictedTaskReasons.keys().next().value;
73898
+ if (oldest) {
73899
+ this.evictedTaskReasons.delete(oldest);
73900
+ }
73156
73901
  }
73157
- };
73158
- }
73159
- function hasProviderConfig(provider, config = getEnvConfig()) {
73160
- switch (provider) {
73161
- case "gemini":
73162
- return Boolean(config.geminiApiKey);
73163
- case "anthropic":
73164
- return Boolean(config.anthropicApiKey);
73165
- case "openai":
73166
- return Boolean(config.openaiApiKey);
73167
- case "xai":
73168
- return Boolean(config.xaiApiKey);
73169
- case "mistral":
73170
- return Boolean(config.mistralApiKey);
73171
- case "deepseek":
73172
- return Boolean(config.deepseekApiKey);
73173
- case "openrouter":
73174
- return Boolean(config.openrouterApiKey);
73175
- case "custom":
73176
- return Boolean(config.custom.baseURL && config.custom.modelName);
73902
+ this.evictedTaskReasons.set(taskId, reason);
73903
+ }
73904
+ getTaskNotFoundError(taskId) {
73905
+ const reason = this.evictedTaskReasons.get(taskId);
73906
+ if (reason) {
73907
+ return new Error(`Task "${taskId}" was ${reason}.`);
73908
+ }
73909
+ return new Error(`Task "${taskId}" not found (task ID does not exist).`);
73177
73910
  }
73178
- }
73179
- function getAvailableProviders(config = getEnvConfig()) {
73180
- const providers = [];
73181
- if (hasProviderConfig("gemini", config))
73182
- providers.push("gemini");
73183
- if (hasProviderConfig("anthropic", config))
73184
- providers.push("anthropic");
73185
- if (hasProviderConfig("openai", config))
73186
- providers.push("openai");
73187
- if (hasProviderConfig("xai", config))
73188
- providers.push("xai");
73189
- if (hasProviderConfig("mistral", config))
73190
- providers.push("mistral");
73191
- if (hasProviderConfig("deepseek", config))
73192
- providers.push("deepseek");
73193
- if (hasProviderConfig("openrouter", config))
73194
- providers.push("openrouter");
73195
- if (hasProviderConfig("custom", config))
73196
- providers.push("custom");
73197
- return providers;
73198
73911
  }
73199
73912
  // src/errors/classifier.ts
73200
73913
  function classifyError(error) {
@@ -73340,15 +74053,15 @@ function classifyError(error) {
73340
74053
  };
73341
74054
  }
73342
74055
  // src/errors/logger.ts
73343
- import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync3 } from "node:fs";
73344
- import { homedir as homedir5 } from "node:os";
73345
- import { join as join13 } from "node:path";
74056
+ import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync4 } from "node:fs";
74057
+ import { homedir as homedir6 } from "node:os";
74058
+ import { join as join14 } from "node:path";
73346
74059
  var SENSITIVE_KEY_REGEX = /api[_-]?key|token|secret|password|auth|credential|bearer/i;
73347
74060
  function getLogsRootDir() {
73348
74061
  if (process.env.STEWARD_LOGS_DIR) {
73349
74062
  return process.env.STEWARD_LOGS_DIR;
73350
74063
  }
73351
- return join13(homedir5(), ".steward", "logs");
74064
+ return join14(homedir6(), ".steward", "logs");
73352
74065
  }
73353
74066
  function getLogDateTime(d = new Date) {
73354
74067
  const year = d.getFullYear();
@@ -73432,11 +74145,11 @@ function logError(error, contextInfo) {
73432
74145
  try {
73433
74146
  const rootDir = getLogsRootDir();
73434
74147
  const { date, time } = getLogDateTime();
73435
- const dateDir = join13(rootDir, date);
73436
- if (!existsSync12(dateDir)) {
73437
- mkdirSync9(dateDir, { recursive: true });
74148
+ const dateDir = join14(rootDir, date);
74149
+ if (!existsSync15(dateDir)) {
74150
+ mkdirSync10(dateDir, { recursive: true });
73438
74151
  }
73439
- const logPath = join13(dateDir, `${time}.log`);
74152
+ const logPath = join14(dateDir, `${time}.log`);
73440
74153
  const structured = classifyError(error);
73441
74154
  const mem = process.memoryUsage ? process.memoryUsage() : undefined;
73442
74155
  const logEntry = {
@@ -73462,10 +74175,10 @@ function logError(error, contextInfo) {
73462
74175
  ` + JSON.stringify(logEntry, null, 2) + `
73463
74176
 
73464
74177
  `;
73465
- if (existsSync12(logPath)) {
74178
+ if (existsSync15(logPath)) {
73466
74179
  appendFileSync3(logPath, formatted, "utf-8");
73467
74180
  } else {
73468
- writeFileSync3(logPath, formatted, "utf-8");
74181
+ writeFileSync4(logPath, formatted, "utf-8");
73469
74182
  }
73470
74183
  return logPath;
73471
74184
  } catch {
@@ -111123,7 +111836,7 @@ function createProviderExecutedToolFactory2({
111123
111836
  supportsDeferredResults
111124
111837
  });
111125
111838
  }
111126
- async function resolve9(value) {
111839
+ async function resolve10(value) {
111127
111840
  if (typeof value === "function") {
111128
111841
  value = value();
111129
111842
  }
@@ -116349,7 +117062,7 @@ var XaiSpeechModel = class _XaiSpeechModel {
116349
117062
  const { requestBody, warnings, withTimestamps } = await this.getArgs(options);
116350
117063
  const { value, responseHeaders, rawValue } = await postJsonToApi4({
116351
117064
  url: `${this.config.baseURL}/tts`,
116352
- headers: combineHeaders4(this.config.headers ? await resolve9(this.config.headers) : undefined, options.headers),
117065
+ headers: combineHeaders4(this.config.headers ? await resolve10(this.config.headers) : undefined, options.headers),
116353
117066
  body: requestBody,
116354
117067
  failedResponseHandler: xaiFailedResponseHandler,
116355
117068
  successfulResponseHandler: withTimestamps ? createJsonResponseHandler4(xaiSpeechTimestampsResponseSchema) : createBinaryResponseHandler2(),
@@ -117156,288 +117869,59 @@ function inferProviderFromModelId(modelId) {
117156
117869
  }
117157
117870
  return null;
117158
117871
  }
117159
- // src/skills/discovery.ts
117160
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "node:fs";
117161
- import { homedir as homedir6 } from "node:os";
117162
- import { join as join14 } from "node:path";
117163
- function parseSkillFrontmatter(content) {
117164
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
117165
- if (!match || !match[1]) {
117166
- return null;
117167
- }
117168
- const rawYaml = match[1];
117169
- const lines = rawYaml.split(/\r?\n/);
117170
- const result = {};
117171
- let currentKey = null;
117172
- let descriptionLines = [];
117173
- for (const line of lines) {
117174
- const trimmed = line.trim();
117175
- if (!trimmed || trimmed.startsWith("#"))
117176
- continue;
117177
- const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
117178
- if (keyMatch && keyMatch[1]) {
117179
- const key = keyMatch[1].toLowerCase();
117180
- const val = (keyMatch[2] ?? "").trim();
117181
- if (key === "name") {
117182
- currentKey = "name";
117183
- result.name = unquote(val);
117184
- } else if (key === "description") {
117185
- currentKey = "description";
117186
- if (val === ">" || val === ">-" || val === "|" || val === "|-") {
117187
- descriptionLines = [];
117188
- } else if (val) {
117189
- descriptionLines = [unquote(val)];
117190
- } else {
117191
- descriptionLines = [];
117192
- }
117193
- } else {
117194
- currentKey = null;
117195
- }
117196
- } else if (currentKey === "description" && (line.startsWith(" ") || line.startsWith("\t"))) {
117197
- descriptionLines.push(trimmed);
117198
- }
117199
- }
117200
- if (descriptionLines.length > 0) {
117201
- result.description = descriptionLines.join(" ").trim();
117202
- }
117203
- return result;
117204
- }
117205
- function unquote(str) {
117206
- const trimmed = str.trim();
117207
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
117208
- return trimmed.slice(1, -1);
117209
- }
117210
- return trimmed;
117211
- }
117212
- function discoverSkills(cwd = process.cwd()) {
117213
- const targets = [
117214
- { dir: join14(cwd, ".agents", "skills"), source: "workspace" },
117215
- { dir: join14(homedir6(), ".agents", "skills"), source: "user-agents" },
117216
- { dir: join14(homedir6(), ".steward", "skills"), source: "user-steward" }
117217
- ];
117218
- const skillMap = new Map;
117219
- for (const target of targets) {
117220
- if (!existsSync13(target.dir)) {
117221
- continue;
117222
- }
117223
- try {
117224
- const entries = readdirSync5(target.dir, { withFileTypes: true });
117225
- for (const entry of entries) {
117226
- if (!entry.isDirectory())
117227
- continue;
117228
- const skillDir = join14(target.dir, entry.name);
117229
- const skillMdPath = join14(skillDir, "SKILL.md");
117230
- const fallbackMdPath = join14(skillDir, "skill.md");
117231
- const resolvedFile = existsSync13(skillMdPath) ? skillMdPath : existsSync13(fallbackMdPath) ? fallbackMdPath : null;
117232
- if (!resolvedFile)
117233
- continue;
117234
- try {
117235
- const raw = readFileSync12(resolvedFile, "utf-8");
117236
- const frontmatter = parseSkillFrontmatter(raw);
117237
- const skillName = frontmatter?.name || entry.name;
117238
- const description = frontmatter?.description || "No description provided.";
117239
- if (!skillMap.has(skillName)) {
117240
- skillMap.set(skillName, {
117241
- name: skillName,
117242
- description,
117243
- filePath: resolvedFile,
117244
- dirPath: skillDir,
117245
- source: target.source
117246
- });
117247
- }
117248
- } catch {}
117249
- }
117250
- } catch {}
117251
- }
117252
- return Array.from(skillMap.values());
117253
- }
117254
- function formatSkillsForSystemPrompt(skills) {
117255
- if (skills.length === 0)
117256
- return "";
117257
- const list = skills.map((s) => `- ${s.name} (${s.filePath}): ${s.description}`).join(`
117258
- `);
117259
- return `<skills>
117260
- You can use specialized 'skills' to help you with complex tasks.
117261
-
117262
- 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.
117263
-
117264
- Available skills:
117265
- ${list}
117266
117872
 
117267
- If a skill seems relevant to your current task, you MUST read its SKILL.md instructions using read_file before proceeding.
117268
- </skills>`;
117269
- }
117270
117873
  // src/engine/system-prompt.ts
117271
117874
  function buildSystemPrompt(options = {}) {
117272
117875
  const cwd = options.cwd ?? process.cwd();
117273
117876
  const platform = process.platform;
117274
- const skills = options.skills ?? discoverSkills(cwd);
117275
117877
  const dateUTC = new Date().toUTCString();
117276
- let isAGitREPO = "no";
117277
- try {
117278
- isAGitREPO = Bun.spawnSync(["git", "status"], { cwd, stdio: ["ignore", "ignore", "ignore"] }).exitCode === 0 ? "yes" : "no";
117279
- } catch {
117280
- isAGitREPO = "no";
117281
- }
117282
- let prompt = `
117283
- 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.
117284
-
117285
- 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.
117286
- NOTE: - To give feedback, users should report the issue at https://github.com/sapirrior/steward/issues
117287
-
117288
- 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
117289
-
117290
- # Tone and style
117291
- You should be concise, direct, and to the point.
117292
- You MUST answer concisely with fewer than 4 lines (not including the tool use or code generation), unless the USER asks for detail.
117293
- 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.
117294
- 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.
117295
- 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.
117296
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
117297
- 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:
117298
-
117299
- <example>
117300
- user: what is 2 + 2
117301
- assistant: 4
117302
- </example>
117303
-
117304
- <example>
117305
- user: 4 + 4
117306
- assistant: 8
117307
- </example>
117308
-
117309
- <example>
117310
- user: what command should I run to read the content of a specific file?
117311
- assistant: cat
117312
- </example>
117313
-
117314
- <example>
117315
- user: what command should I run to watch files in the current directory?
117316
- [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]
117317
- assistant: npm run dev
117318
- </example>
117319
-
117320
- <example>
117321
- user: can you find all the TODO comments in src/
117322
- [assistant runs search text tool call and sees TODO comment occurance in src/foo.c at line 11 src/boo.c at 180]
117323
- assistant: src/foo:11 and src/boo.c:180
117324
- </example>
117325
-
117326
- 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).
117327
- 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.
117328
- 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.
117329
- IMPORTANT: Only use emojis if the USER explicitly requests it. Avoid using emojis in all communication unless asked.
117330
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
117331
-
117332
- # Proactiveness
117333
- You are allowed to be proactive, but only when the USER asks you to do something. You should strive to strike a balance between:
117334
- - Doing the right thing when asked, including taking actions and follow-up actions
117335
- - Not surprising the USER with actions you take without asking
117336
- 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.
117337
-
117338
- # Following conventions
117339
- When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
117340
- - 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).
117341
- - 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.
117342
- - 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.
117343
- - Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
117344
- - 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.
117345
-
117346
- # Code style
117347
- - IMPORTANT: DO NOT ADD **ANY** COMMENTS unless explicitly asked.
117878
+ 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.
117348
117879
 
117349
- # Tool usage policy
117350
- - Use the edit file and write file tools for both normal code/file changes as well as making plans and todos.
117351
- - 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.
117352
- - When web search returns but does not contain enough data, use the URLs from the results and WebFetch them for more detailed information.
117353
- - 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 '
117354
- ' to submit a line / press Enter), and task_kill to terminate it.
117880
+ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code intended for malicious use.
117355
117881
 
117356
- # Task Management (Planning)
117357
- 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.
117358
- 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.
117359
- 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.
117360
- 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.
117361
- 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).
117362
- 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.
117882
+ # Operating Principles
117883
+ - Inspect before editing: investigate existing files, directories, and patterns before making changes.
117884
+ - Minimal and idiomatic: prefer the smallest correct change that aligns with existing repository architecture and conventions.
117885
+ - Never assume external dependencies or libraries exist; check project configuration files (e.g. package.json, Cargo.toml, pyproject.toml) first.
117886
+ - Security and secrets: never expose, commit, or log API keys, credentials, or secrets.
117887
+ - Verify your changes: run relevant project verification or tests after making non-trivial modifications.
117888
+ - Never commit git changes unless the user explicitly requests you to commit.
117889
+ - Provide direct, concise responses suitable for a terminal interface. Explain non-obvious failures or decisions clearly without unnecessary filler or commentary.
117890
+ - When referencing code locations, use the format: \`file_path:line_number\`.
117891
+ - Avoid adding redundant comments unless explaining non-obvious invariants or explicitly requested.
117363
117892
 
117364
- <example>
117365
- user: Run the build and fix any type errors.
117366
- assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
117367
- - Run the build
117368
- - Fix any type errors
117369
- [assistant uses write_file to write the task todo/plan in [project dir]/.steward/plans/fixing_type_errors_plan.md]
117370
- assistant: I'm now going to run the build using Bash.
117371
- [assistant runs the build command and gets 10 type errors]
117372
- 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.
117373
- [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]
117374
- assistant: Marking the first todo as in_progress.
117375
- [assistant uses the edit file to mark 'run build command to find type errors' todo]
117376
- Let me start working on the first item...
117377
- [assistant reads the first file, uses edit tool and fixes it]
117378
- The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
117379
- [same as first one, assistant fixes all files]
117380
- </example>
117381
- In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build to double-check the fixes.
117893
+ # Tool Usage Policy
117894
+ - Investigation: use read_file, grep, list_dir, and glob to explore code before modifying it.
117895
+ - Mutations: ALWAYS use write_file and edit_file to create or modify files rather than bash redirection (e.g. cat > file). write_file and edit_file participate in the file checkpoint and rewind system (/rewind), whereas bash commands are not tracked by checkpoints.
117896
+ - Commands: when using bash, you MUST always provide a concise, non-empty 'explanation' parameter describing what the command does and why it is needed.
117897
+ - Background tasks: use task_list to inspect active and recent tasks, task_read to check status/output, task_send_input to send standard input (with a trailing newline), and task_kill to terminate.
117898
+ - 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.
117382
117899
 
117383
- <example>
117384
- user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
117900
+ # Task and Plan Management (Todos)
117901
+ - For multi-step, complex, or verification-heavy tasks, maintain operational state using session todos:
117902
+ - todo_write (TodoWrite): initialize or replace the session todo list (must contain 2 to 10 items).
117903
+ - todo_update (TodoUpdate): update exactly one todo item status or description as progress occurs.
117904
+ - todo_read (TodoRead): inspect current session todos when recovering or verifying progress.
117905
+ - Keep at most one item in 'in_progress' status at any time.
117906
+ - Mark todos completed immediately after completing each step; do not batch progress updates.
117907
+ - For simple, one-step queries or edits, do not create a todo list.
117385
117908
 
117386
- 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.
117387
- Adding the following todos to the todo list:
117388
- 1. Research existing metrics tracking in the codebase
117389
- 2. Design the metrics collection system
117390
- 3. Implement core metrics tracking functionality
117391
- 4. Create export functionality for different formats
117392
- [assistant uses the write file to write the plan and its todos in [project dir]/.steward/plans/]
117393
- Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
117394
- [assistant lists all files and reads all the relevant files step by step]
117395
- I'm going to search for any existing metrics or telemetry code in the project.
117396
- [assistant runs search text to find any existing telemetry code in the codebase]
117397
- 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...
117398
- [assistant continues implementing the feature step by step, marking todos as completed as they go]
117399
- </example>
117909
+ # Specialized Skills Policy
117910
+ - Skills are discoverable on-demand packages of specialized instructions located in workspace or user configuration.
117911
+ - Use skill_list (SkillList) to discover available skills and their descriptions.
117912
+ - Use skill_read (SkillRead) to load a skill's instructions (SKILL.md) or relative resources only when a task requires specialized domain guidance.
117913
+ - Skill instructions are supplemental and must not override core safety rules, tool boundaries, or user permission constraints.
117400
117914
 
117401
- IMPORTANT: Completed todo: marked with ◉ beside the todo text and * beside the current working todo.
117402
-
117403
- # Doing tasks
117404
- 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:
117405
- - Use the write file tool to plan the task if required.
117406
- - 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.
117407
- - Implement the solution using all tools available to you.
117408
- - 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.
117409
- - 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.
117410
- 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.
117411
-
117412
- Here is useful information about the environment you are running in:
117413
- <env>
117414
- Working directory: ${cwd}
117415
- Is directory a git repo: ${isAGitREPO}
117416
- Platform: ${platform}
117417
- Today's date: ${dateUTC}
117418
- </env>
117419
-
117420
- # Code References
117421
- 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.
117422
-
117423
- <example>
117424
- user: Where are errors from the client handled?
117425
- assistant: Clients are marked as failed in the \`connectToServer\` function in src/services/process.ts:712.
117426
- </example>`;
117427
- const skillsPrompt = formatSkillsForSystemPrompt(skills);
117428
- if (skillsPrompt) {
117429
- prompt += `
117430
-
117431
- ${skillsPrompt}`;
117432
- }
117915
+ <env>
117916
+ Working directory: ${cwd}
117917
+ Platform: ${platform}
117918
+ Today's date: ${dateUTC}
117919
+ </env>`;
117433
117920
  if (options.userRules && options.userRules.length > 0) {
117434
117921
  prompt += `
117435
117922
 
117436
117923
  <user_defined_rules>
117437
- The following rules are set by the user and MUST be followed without exception.
117438
- They take precedence over all other style and behaviour guidelines above,
117439
- except for the operating_principles and security sections.
117440
-
117924
+ The following rules are configured by the user and must be followed:
117441
117925
  ${options.userRules.map((rule) => `- ${rule}`).join(`
117442
117926
  `)}
117443
117927
  </user_defined_rules>`;
@@ -117601,6 +118085,7 @@ class AgentSession {
117601
118085
  await tracker.beginTurn(turnId, this.sessionData.turns.length + 1);
117602
118086
  const toolContext = {
117603
118087
  cwd,
118088
+ sessionId: this.sessionData.id,
117604
118089
  abortSignal: this.activeAbortController.signal,
117605
118090
  checkpointTracker: tracker,
117606
118091
  mutationLocks: globalMutationLockManager,
@@ -117617,6 +118102,7 @@ class AgentSession {
117617
118102
  activeTools = defaultToolCatalog.toAISDKTools(toolContext);
117618
118103
  }
117619
118104
  const instructions = buildSystemPrompt({
118105
+ cwd,
117620
118106
  extraInstructions: options.extraInstructions
117621
118107
  });
117622
118108
  const wrappedOnEvent = (event) => {
@@ -119068,6 +119554,7 @@ class StatusBar extends Component {
119068
119554
  clip = true;
119069
119555
  warningTimer = null;
119070
119556
  voiceTimer = null;
119557
+ updateTimer = null;
119071
119558
  constructor(props) {
119072
119559
  super(props);
119073
119560
  this.state = {
@@ -119077,7 +119564,8 @@ class StatusBar extends Component {
119077
119564
  exitPending: props.exitPending,
119078
119565
  warning: props.warning,
119079
119566
  voiceState: props.voiceState ?? "idle",
119080
- voiceDurationSec: props.voiceDurationSec ?? 0
119567
+ voiceDurationSec: props.voiceDurationSec ?? 0,
119568
+ updateStatus: props.updateStatus
119081
119569
  };
119082
119570
  }
119083
119571
  update(partial) {
@@ -119100,8 +119588,20 @@ class StatusBar extends Component {
119100
119588
  });
119101
119589
  }
119102
119590
  }
119103
- setUpdateStatus(updateStatus) {
119591
+ setUpdateStatus(updateStatus, durationMs) {
119592
+ if (this.updateTimer) {
119593
+ clearTimeout(this.updateTimer);
119594
+ this.updateTimer = null;
119595
+ }
119104
119596
  this.setState({ updateStatus });
119597
+ if (updateStatus && durationMs && durationMs > 0) {
119598
+ this.updateTimer = setTimeout(() => {
119599
+ this.updateTimer = null;
119600
+ if (this.state.updateStatus === updateStatus) {
119601
+ this.setState({ updateStatus: undefined });
119602
+ }
119603
+ }, durationMs);
119604
+ }
119105
119605
  }
119106
119606
  showWarning(warningText, durationMs = 4000) {
119107
119607
  if (this.warningTimer) {
@@ -119125,6 +119625,10 @@ class StatusBar extends Component {
119125
119625
  clearInterval(this.voiceTimer);
119126
119626
  this.voiceTimer = null;
119127
119627
  }
119628
+ if (this.updateTimer) {
119629
+ clearTimeout(this.updateTimer);
119630
+ this.updateTimer = null;
119631
+ }
119128
119632
  }
119129
119633
  render(width) {
119130
119634
  const theme = getTheme();
@@ -119158,14 +119662,10 @@ class StatusBar extends Component {
119158
119662
  } else if (updateStatus && updateStatus.state !== "idle") {
119159
119663
  if (updateStatus.state === "checking") {
119160
119664
  left = source_default.white(updateStatus.message ?? "Checking for updates...");
119161
- } else if (updateStatus.state === "no-updates") {
119162
- left = source_default.white(updateStatus.message ?? "No updates found");
119163
119665
  } else if (updateStatus.state === "available") {
119164
- left = source_default.white(updateStatus.message ?? `Found version v${updateStatus.version ?? ""}`);
119165
- } else if (updateStatus.state === "downloading") {
119166
- left = source_default.white(updateStatus.message ?? `Downloading v${updateStatus.version ?? ""}...`);
119167
- } else if (updateStatus.state === "ready") {
119168
- left = source_default.white.bold(updateStatus.message ?? "Update complete · Restart to apply");
119666
+ left = source_default.white(updateStatus.message ?? `Update v${updateStatus.version ?? ""} is available`);
119667
+ } else if (updateStatus.state === "no-updates") {
119668
+ left = source_default.white(updateStatus.message ?? "Steward is up to date");
119169
119669
  } else if (updateStatus.state === "error" && updateStatus.message) {
119170
119670
  left = themeColor2(theme.error)(updateStatus.message);
119171
119671
  } else if (isBusy) {
@@ -119306,6 +119806,8 @@ class StreamingView extends Component {
119306
119806
  const maxArgLen = Math.max(10, maxCols - toolName.length - 8);
119307
119807
  const truncatedArg = targetArg.length > maxArgLen ? truncateMiddle(targetArg, maxArgLen) : targetArg;
119308
119808
  line += `${source_default.dim("(")}${source_default.dim(truncatedArg)}${source_default.dim(")")}`;
119809
+ } else {
119810
+ line += `${source_default.dim("()")}`;
119309
119811
  }
119310
119812
  lines.push(line);
119311
119813
  if (activeTool.recentLines && activeTool.recentLines.length > 0) {
@@ -120088,6 +120590,9 @@ class TrustGate extends Component {
120088
120590
 
120089
120591
  // src/tui/components/docks/ModelPicker.tsx
120090
120592
  class ModelPicker extends SelectList {
120593
+ wrap = false;
120594
+ clip = true;
120595
+ ellipsis = false;
120091
120596
  constructor(props) {
120092
120597
  super({
120093
120598
  items: props.models,
@@ -120136,6 +120641,9 @@ class ModelPicker extends SelectList {
120136
120641
 
120137
120642
  // src/tui/components/docks/SessionMenu.tsx
120138
120643
  class SessionMenu extends SelectList {
120644
+ wrap = false;
120645
+ clip = true;
120646
+ ellipsis = false;
120139
120647
  constructor(props) {
120140
120648
  super({
120141
120649
  items: props.sessions,
@@ -120518,6 +121026,9 @@ function buildRewindItems(session, cwd) {
120518
121026
  }
120519
121027
 
120520
121028
  class RewindMenu extends SelectList {
121029
+ wrap = false;
121030
+ clip = true;
121031
+ ellipsis = false;
120521
121032
  constructor(props) {
120522
121033
  const items = buildRewindItems(props.session, props.cwd);
120523
121034
  super({
@@ -121139,6 +121650,15 @@ class FilePermissionDock extends Component {
121139
121650
  }
121140
121651
 
121141
121652
  // src/tui/utils/permission-queue.ts
121653
+ function settleItem(item, allowed) {
121654
+ item.settled = true;
121655
+ if (item.kind === "bash") {
121656
+ item.resolve({ allowed });
121657
+ } else {
121658
+ item.resolve({ allowed });
121659
+ }
121660
+ }
121661
+
121142
121662
  class PermissionQueue {
121143
121663
  queue = [];
121144
121664
  activeItem = null;
@@ -121209,28 +121729,25 @@ class PermissionQueue {
121209
121729
  if (!current || current.settled) {
121210
121730
  return;
121211
121731
  }
121212
- current.settled = true;
121213
121732
  this.activeItem = null;
121214
121733
  this.onHide();
121215
- current.resolve({ allowed });
121734
+ settleItem(current, allowed);
121216
121735
  this.processNext();
121217
121736
  }
121218
121737
  cancelItem(item) {
121219
121738
  if (item.settled)
121220
121739
  return;
121221
121740
  if (this.activeItem === item) {
121222
- item.settled = true;
121223
121741
  this.activeItem = null;
121224
121742
  this.onHide();
121225
- item.resolve({ allowed: false });
121743
+ settleItem(item, false);
121226
121744
  this.processNext();
121227
121745
  } else {
121228
121746
  const idx = this.queue.indexOf(item);
121229
121747
  if (idx !== -1) {
121230
121748
  this.queue.splice(idx, 1);
121231
121749
  }
121232
- item.settled = true;
121233
- item.resolve({ allowed: false });
121750
+ settleItem(item, false);
121234
121751
  }
121235
121752
  }
121236
121753
  processNext() {
@@ -121238,8 +121755,7 @@ class PermissionQueue {
121238
121755
  const next = this.queue.shift();
121239
121756
  if (next.settled || next.abortSignal?.aborted) {
121240
121757
  if (!next.settled) {
121241
- next.settled = true;
121242
- next.resolve({ allowed: false });
121758
+ settleItem(next, false);
121243
121759
  }
121244
121760
  continue;
121245
121761
  }
@@ -121251,16 +121767,15 @@ class PermissionQueue {
121251
121767
  }
121252
121768
  clear() {
121253
121769
  if (this.activeItem && !this.activeItem.settled) {
121254
- this.activeItem.settled = true;
121255
- this.activeItem.resolve({ allowed: false });
121770
+ const active = this.activeItem;
121256
121771
  this.activeItem = null;
121257
121772
  this.onHide();
121773
+ settleItem(active, false);
121258
121774
  }
121259
121775
  while (this.queue.length > 0) {
121260
121776
  const item = this.queue.shift();
121261
121777
  if (!item.settled) {
121262
- item.settled = true;
121263
- item.resolve({ allowed: false });
121778
+ settleItem(item, false);
121264
121779
  }
121265
121780
  }
121266
121781
  }
@@ -121347,17 +121862,19 @@ function classifyVoiceError(err) {
121347
121862
  };
121348
121863
  }
121349
121864
  // src/voice/prerequisites.ts
121865
+ import { spawn as spawn2 } from "node:child_process";
121350
121866
  async function defaultCheckParec() {
121351
- try {
121352
- const proc = Bun.spawn(["which", "parec"], {
121353
- stdout: "ignore",
121354
- stderr: "ignore"
121355
- });
121356
- const exitCode = await proc.exited;
121357
- return exitCode === 0;
121358
- } catch {
121359
- return false;
121360
- }
121867
+ return new Promise((resolve) => {
121868
+ try {
121869
+ const proc = spawn2("which", ["parec"], {
121870
+ stdio: "ignore"
121871
+ });
121872
+ proc.on("error", () => resolve(false));
121873
+ proc.on("close", (code) => resolve(code === 0));
121874
+ } catch {
121875
+ resolve(false);
121876
+ }
121877
+ });
121361
121878
  }
121362
121879
  async function checkVoicePrerequisites(options = {}) {
121363
121880
  const config = options.config ?? getEnvConfig();
@@ -121425,6 +121942,8 @@ class TranscriptAccumulator {
121425
121942
  }
121426
121943
  }
121427
121944
  // src/voice/recorder.ts
121945
+ import { spawn as spawn3 } from "node:child_process";
121946
+ import { Readable } from "node:stream";
121428
121947
  var DEFAULT_PAREC_ARGS = ["--raw", "--rate=16000", "--channels=1", "--format=s16le"];
121429
121948
 
121430
121949
  class ParecAudioRecorder {
@@ -121436,12 +121955,40 @@ class ParecAudioRecorder {
121436
121955
  checkAvailableFn;
121437
121956
  constructor(options) {
121438
121957
  this.spawnFn = options?.spawnFn ?? ((cmd, opts) => {
121439
- const p = Bun.spawn(cmd, opts);
121958
+ const [executable, ...args] = cmd;
121959
+ const child = spawn3(executable, args, {
121960
+ stdio: [
121961
+ "ignore",
121962
+ opts.stdout === "pipe" ? "pipe" : "ignore",
121963
+ opts.stderr === "pipe" ? "pipe" : "ignore"
121964
+ ]
121965
+ });
121966
+ const exited = new Promise((resolve) => {
121967
+ child.on("close", (code) => resolve(code ?? 0));
121968
+ child.on("error", () => resolve(1));
121969
+ });
121970
+ const stdout = child.stdout ? Readable.toWeb(child.stdout) : null;
121971
+ const stderr = child.stderr ? Readable.toWeb(child.stderr) : null;
121440
121972
  return {
121441
- stdout: p.stdout,
121442
- stderr: p.stderr,
121443
- exited: p.exited,
121444
- kill: (sig) => p.kill(sig)
121973
+ stdout,
121974
+ stderr,
121975
+ exited,
121976
+ kill: (sig) => {
121977
+ try {
121978
+ if (typeof sig === "number") {
121979
+ const signalMap = {
121980
+ 2: "SIGINT",
121981
+ 9: "SIGKILL",
121982
+ 15: "SIGTERM"
121983
+ };
121984
+ child.kill(signalMap[sig] || sig);
121985
+ } else if (sig) {
121986
+ child.kill(sig);
121987
+ } else {
121988
+ child.kill();
121989
+ }
121990
+ } catch {}
121991
+ }
121445
121992
  };
121446
121993
  });
121447
121994
  this.checkAvailableFn = options?.checkAvailableFn ?? defaultCheckParec;
@@ -122088,9 +122635,11 @@ class VoiceController {
122088
122635
  }
122089
122636
  }
122090
122637
  // src/services/updater/service.ts
122091
- import { spawn as spawn2 } from "node:child_process";
122092
122638
  var DEFAULT_REPO = "sapirrior/steward";
122093
122639
  function parseSemver(v) {
122640
+ if (!v || typeof v !== "string") {
122641
+ return [0, 0, 0];
122642
+ }
122094
122643
  const clean = v.replace(/^v/i, "").trim().split("-")[0] ?? "";
122095
122644
  const parts = clean.split(".").map((p) => parseInt(p, 10) || 0);
122096
122645
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
@@ -122109,14 +122658,13 @@ function isNewerVersion(current, latest) {
122109
122658
  return lPat > cPat;
122110
122659
  }
122111
122660
 
122112
- class AutoUpdaterService {
122661
+ class UpdateCheckerService {
122113
122662
  currentVersion;
122114
122663
  repo;
122115
122664
  state = "idle";
122116
122665
  options;
122117
122666
  checkTimer = null;
122118
- intervalTimer = null;
122119
- isUpdating = false;
122667
+ resetTimer = null;
122120
122668
  constructor(options = {}) {
122121
122669
  this.options = options;
122122
122670
  this.currentVersion = options.currentVersion || package_default.version || "0.0.0";
@@ -122131,14 +122679,8 @@ class AutoUpdaterService {
122131
122679
  getCurrentVersion() {
122132
122680
  return this.currentVersion;
122133
122681
  }
122134
- get latestReleaseApi() {
122135
- return `https://api.github.com/repos/${this.repo}/releases/latest`;
122136
- }
122137
- get installShUrl() {
122138
- return `https://raw.githubusercontent.com/${this.repo}/main/installer/install.sh`;
122139
- }
122140
- get installPs1Url() {
122141
- return `https://raw.githubusercontent.com/${this.repo}/main/installer/install.ps1`;
122682
+ get rawPackageJsonUrl() {
122683
+ return `https://raw.githubusercontent.com/${this.repo}/main/package.json`;
122142
122684
  }
122143
122685
  setState(state, info) {
122144
122686
  this.state = state;
@@ -122148,156 +122690,94 @@ class AutoUpdaterService {
122148
122690
  if (process.env.STEWARD_NO_UPDATE_CHECK === "1" || process.env.STEWARD_NO_UPDATE_CHECK === "true") {
122149
122691
  return null;
122150
122692
  }
122693
+ if (this.resetTimer) {
122694
+ clearTimeout(this.resetTimer);
122695
+ this.resetTimer = null;
122696
+ }
122151
122697
  try {
122152
122698
  this.setState("checking", { message: "Checking for updates..." });
122699
+ const checkStartTime = Date.now();
122153
122700
  const controller = new AbortController;
122154
122701
  const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 1e4);
122155
- const response = await fetch(this.latestReleaseApi, {
122702
+ const response = await fetch(this.rawPackageJsonUrl, {
122156
122703
  headers: {
122157
122704
  "User-Agent": `steward-cli/${this.currentVersion}`,
122158
- Accept: "application/vnd.github.v3+json"
122705
+ Accept: "application/json"
122159
122706
  },
122160
122707
  signal: controller.signal
122161
122708
  });
122162
122709
  clearTimeout(timeout);
122710
+ const elapsed = Date.now() - checkStartTime;
122711
+ const targetMinDisplay = this.options.minDisplayMs !== undefined ? this.options.minDisplayMs : 1000;
122712
+ const minDisplayDelay = Math.max(0, targetMinDisplay - elapsed);
122713
+ if (minDisplayDelay > 0) {
122714
+ await new Promise((resolve) => setTimeout(resolve, minDisplayDelay));
122715
+ }
122163
122716
  if (!response.ok) {
122164
- this.setState("idle");
122717
+ this.setState("error", { message: "Failed to check for updates" });
122718
+ this.resetTimer = setTimeout(() => {
122719
+ if (this.state === "error")
122720
+ this.setState("idle");
122721
+ }, 3000);
122165
122722
  return null;
122166
122723
  }
122167
122724
  const data = await response.json();
122168
- const rawTag = data.tag_name ?? "";
122169
- const latestVersion = rawTag.replace(/^v/i, "").trim();
122725
+ if (!data || typeof data !== "object" || typeof data.version !== "string") {
122726
+ this.setState("error", { message: "Invalid upstream version response" });
122727
+ this.resetTimer = setTimeout(() => {
122728
+ if (this.state === "error")
122729
+ this.setState("idle");
122730
+ }, 3000);
122731
+ return null;
122732
+ }
122733
+ const latestVersion = data.version.replace(/^v/i, "").trim();
122170
122734
  if (!latestVersion) {
122171
- this.setState("idle");
122735
+ this.setState("error", { message: "Empty upstream version response" });
122736
+ this.resetTimer = setTimeout(() => {
122737
+ if (this.state === "error")
122738
+ this.setState("idle");
122739
+ }, 3000);
122172
122740
  return null;
122173
122741
  }
122174
122742
  const hasUpdate = isNewerVersion(this.currentVersion, latestVersion);
122175
122743
  const updateInfo = {
122176
122744
  currentVersion: this.currentVersion,
122177
122745
  latestVersion,
122178
- hasUpdate,
122179
- tag: rawTag,
122180
- releaseUrl: data.html_url
122746
+ hasUpdate
122181
122747
  };
122182
122748
  if (hasUpdate) {
122183
122749
  this.setState("available", {
122184
122750
  version: latestVersion,
122185
- message: `Found version v${latestVersion}`
122186
- });
122187
- } else {
122188
- this.setState("no-updates", { message: "No updates found" });
122189
- setTimeout(() => {
122190
- if (this.state === "no-updates") {
122191
- this.setState("idle");
122192
- }
122193
- }, 3000);
122194
- }
122195
- return updateInfo;
122196
- } catch (err) {
122197
- logError(err, { component: "AutoUpdaterService", method: "checkForUpdates" });
122198
- this.setState("idle");
122199
- return null;
122200
- }
122201
- }
122202
- async installUpdate(version) {
122203
- if (this.isUpdating)
122204
- return false;
122205
- this.isUpdating = true;
122206
- this.setState("downloading", {
122207
- version,
122208
- message: `Downloading v${version}...`
122209
- });
122210
- try {
122211
- const isWindows = process.platform === "win32";
122212
- let childProcess;
122213
- if (isWindows) {
122214
- childProcess = spawn2("powershell", [
122215
- "-NoProfile",
122216
- "-ExecutionPolicy",
122217
- "Bypass",
122218
- "-Command",
122219
- `irm ${this.installPs1Url} | iex`
122220
- ], {
122221
- stdio: "ignore",
122222
- windowsHide: true
122751
+ message: `Update v${latestVersion} is available`
122223
122752
  });
122224
122753
  } else {
122225
- childProcess = spawn2("bash", ["-c", `curl -fsSL ${this.installShUrl} | bash`], {
122226
- stdio: "ignore",
122227
- env: {
122228
- ...process.env,
122229
- HOME: process.env.HOME || "",
122230
- PATH: process.env.PATH || ""
122231
- }
122754
+ this.setState("no-updates", {
122755
+ version: this.currentVersion,
122756
+ message: `Steward is up to date (v${this.currentVersion})`
122232
122757
  });
122233
- }
122234
- const success = await new Promise((resolve) => {
122235
- const timeout = setTimeout(() => {
122236
- try {
122237
- childProcess.kill();
122238
- } catch {}
122239
- resolve(false);
122240
- }, 300000);
122241
- childProcess.on("exit", (code) => {
122242
- clearTimeout(timeout);
122243
- resolve(code === 0);
122244
- });
122245
- childProcess.on("error", () => {
122246
- clearTimeout(timeout);
122247
- resolve(false);
122248
- });
122249
- });
122250
- if (success) {
122251
- this.setState("ready", {
122252
- version,
122253
- message: `Update complete · Restart to apply`
122254
- });
122255
- return true;
122256
- } else {
122257
- this.setState("error", {
122258
- version,
122259
- message: `Update to v${version} failed`
122260
- });
122261
- setTimeout(() => {
122262
- if (this.state === "error") {
122758
+ this.resetTimer = setTimeout(() => {
122759
+ if (this.state === "no-updates")
122263
122760
  this.setState("idle");
122264
- }
122265
- }, 4000);
122266
- return false;
122761
+ }, 3500);
122267
122762
  }
122763
+ return updateInfo;
122268
122764
  } catch (err) {
122269
- logError(err, { component: "AutoUpdaterService", method: "installUpdate" });
122270
- this.setState("error", { version, message: "Update failed" });
122271
- setTimeout(() => {
122272
- if (this.state === "error") {
122765
+ logError(err, { component: "UpdateCheckerService", method: "checkForUpdates" });
122766
+ this.setState("error", { message: "Failed to check for updates" });
122767
+ this.resetTimer = setTimeout(() => {
122768
+ if (this.state === "error")
122273
122769
  this.setState("idle");
122274
- }
122275
- }, 4000);
122276
- return false;
122277
- } finally {
122278
- this.isUpdating = false;
122770
+ }, 3000);
122771
+ return null;
122279
122772
  }
122280
122773
  }
122281
- startBackgroundCheck(initialDelayMs = 2000, intervalMs) {
122282
- if (this.checkTimer)
122774
+ startBackgroundCheck(initialDelayMs = 2000) {
122775
+ if (this.checkTimer) {
122283
122776
  clearTimeout(this.checkTimer);
122284
- if (this.intervalTimer)
122285
- clearInterval(this.intervalTimer);
122286
- const performCheck = async () => {
122287
- const update = await this.checkForUpdates();
122288
- if (update && update.hasUpdate) {
122289
- setTimeout(async () => {
122290
- await this.installUpdate(update.latestVersion);
122291
- }, 1200);
122292
- }
122293
- };
122777
+ }
122294
122778
  this.checkTimer = setTimeout(async () => {
122295
122779
  this.checkTimer = null;
122296
- await performCheck();
122297
- const recurringInterval = intervalMs ?? this.options.checkIntervalMs;
122298
- if (recurringInterval && recurringInterval > 0) {
122299
- this.intervalTimer = setInterval(performCheck, recurringInterval);
122300
- }
122780
+ await this.checkForUpdates();
122301
122781
  }, initialDelayMs);
122302
122782
  }
122303
122783
  dispose() {
@@ -122305,9 +122785,9 @@ class AutoUpdaterService {
122305
122785
  clearTimeout(this.checkTimer);
122306
122786
  this.checkTimer = null;
122307
122787
  }
122308
- if (this.intervalTimer) {
122309
- clearInterval(this.intervalTimer);
122310
- this.intervalTimer = null;
122788
+ if (this.resetTimer) {
122789
+ clearTimeout(this.resetTimer);
122790
+ this.resetTimer = null;
122311
122791
  }
122312
122792
  }
122313
122793
  }
@@ -122326,7 +122806,7 @@ class TUIApp {
122326
122806
  ctrlCTimer = null;
122327
122807
  isBusy = false;
122328
122808
  voiceController;
122329
- autoUpdater;
122809
+ updateChecker;
122330
122810
  permissionQueue;
122331
122811
  constructor(options = {}) {
122332
122812
  this.cwd = options.cwd ?? process.cwd();
@@ -122389,11 +122869,23 @@ class TUIApp {
122389
122869
  this.statusBar.showWarning(warning);
122390
122870
  }
122391
122871
  });
122392
- this.autoUpdater = new AutoUpdaterService({
122872
+ this.updateChecker = new UpdateCheckerService({
122393
122873
  currentVersion: options.version,
122394
122874
  onStatusChange: (state, info) => {
122395
122875
  if (state === "idle") {
122396
122876
  this.statusBar.setUpdateStatus(undefined);
122877
+ } else if (state === "available") {
122878
+ this.statusBar.setUpdateStatus({
122879
+ state,
122880
+ version: info?.version,
122881
+ message: info?.message
122882
+ }, 8000);
122883
+ } else if (state === "error") {
122884
+ this.statusBar.setUpdateStatus({
122885
+ state,
122886
+ version: info?.version,
122887
+ message: info?.message
122888
+ }, 4000);
122397
122889
  } else {
122398
122890
  this.statusBar.setUpdateStatus({
122399
122891
  state,
@@ -122499,7 +122991,7 @@ class TUIApp {
122499
122991
  this.engine.mount(this.streamingView, { kind: "custom" });
122500
122992
  this.engine.mount(this.promptInput, { keepCursorVisible: true, kind: "input" });
122501
122993
  this.engine.mount(this.statusBar, { kind: "custom" });
122502
- this.autoUpdater.startBackgroundCheck(3000);
122994
+ this.updateChecker.startBackgroundCheck(3000);
122503
122995
  this.engine.addInputListener((chunk) => {
122504
122996
  const action = parseKeyInput(chunk);
122505
122997
  if (this.voiceController.isActive) {
@@ -122935,7 +123427,7 @@ class TUIApp {
122935
123427
  this.permissionQueue.clear();
122936
123428
  this.session.shutdown().catch(() => {});
122937
123429
  this.voiceController.dispose();
122938
- this.autoUpdater.dispose();
123430
+ this.updateChecker.dispose();
122939
123431
  this.engine.cleanupSync();
122940
123432
  if (this.onExitCallback) {
122941
123433
  this.onExitCallback();