steward-cli 0.11.0 → 0.12.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 +557 -278
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Steward
2
2
 
3
- [![Version](https://img.shields.io/badge/version-v0.11.0-D77757.svg)](https://github.com/sapirrior/steward/releases)
3
+ [![Version](https://img.shields.io/badge/version-v0.12.0-D77757.svg)](https://github.com/sapirrior/steward/releases)
4
4
  [![npm](https://img.shields.io/npm/v/steward-cli.svg?color=373737)](https://www.npmjs.com/package/steward-cli)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  [![Runtime](https://img.shields.io/badge/runtime-Bun-fbf0df.svg?logo=bun)](https://bun.sh)
package/dist/cli.js CHANGED
@@ -44696,15 +44696,6 @@ function wrapVisualLineWithCursor(text, maxCols, targetCharOffset, hangingIndent
44696
44696
  }
44697
44697
  function emitCurrentLine() {
44698
44698
  const activeStyles = getActiveStyleCodes();
44699
- if (activeBg && currentWidth < maxCols) {
44700
- const remainingCols = maxCols - currentWidth;
44701
- currentTokens.push({
44702
- type: "char",
44703
- value: " ".repeat(remainingCols),
44704
- width: remainingCols
44705
- });
44706
- currentWidth = maxCols;
44707
- }
44708
44699
  let lineStr = currentTokens.map((t) => t.value).join("");
44709
44700
  if (activeStyles.length > 0 && !lineStr.endsWith("\x1B[0m")) {
44710
44701
  lineStr += "\x1B[0m";
@@ -52415,23 +52406,12 @@ function formatToolStatus(options) {
52415
52406
  const maxOutLen = Math.max(10, fullTermWidth - 6);
52416
52407
  const truncatedSummary = summaryText.length > maxOutLen ? `${summaryText.slice(0, maxOutLen - 1)}…` : summaryText;
52417
52408
  lines.push(` ${source_default.dim("└ ")}${source_default.white(truncatedSummary)}`);
52418
- const diffDeleteBg = themeBgColor(theme.diffDeleteBG);
52419
- const diffAddBg = themeBgColor(theme.diffAddBG);
52420
- const rawDiffDelBg = theme.diffDeleteBG;
52421
- const rawDiffAddBg = theme.diffAddBG;
52409
+ const maxContentWidth = Math.max(30, Math.min(fullTermWidth - 4, Math.floor(fullTermWidth * 0.85)));
52422
52410
  for (let i = 1;i < outputLines.length; i++) {
52423
- const line = outputLines[i] ?? "";
52424
- const plain = stripAnsi(line);
52425
- const isDelete = line.includes(rawDiffDelBg) || /\d+\s+-/.test(plain) || plain.startsWith("-");
52426
- const isAdd = line.includes(rawDiffAddBg) || /\d+\s+\+/.test(plain) || plain.startsWith("+");
52427
- if (isDelete || isAdd) {
52428
- const bgFn = isDelete ? diffDeleteBg : diffAddBg;
52429
- const visLen = 3 + stringWidth(plain);
52430
- const padLen = Math.max(0, fullTermWidth - visLen);
52431
- lines.push(bgFn(` ${line}${" ".repeat(padLen)}`));
52432
- } else {
52433
- lines.push(` ${line}`);
52434
- }
52411
+ const rawLine = outputLines[i] ?? "";
52412
+ const prefixed = ` ${rawLine}`;
52413
+ const wrapped = wrapVisualLine(prefixed, maxContentWidth, " ");
52414
+ lines.push(...wrapped);
52435
52415
  }
52436
52416
  }
52437
52417
  }
@@ -71099,7 +71079,8 @@ var writeFileTool = {
71099
71079
  summarize: (args, result) => {
71100
71080
  const lines = result?.linesWritten ?? 0;
71101
71081
  const filePath = args.file_path;
71102
- const summary = `Wrote ${lines} line${lines === 1 ? "" : "s"} to ${filePath}`;
71082
+ const isNew = result?.isNew ?? true;
71083
+ const summary = isNew ? `Created ${filePath} (${lines} line${lines === 1 ? "" : "s"})` : `Wrote ${lines} line${lines === 1 ? "" : "s"} to ${filePath}`;
71103
71084
  const contentLines = args.content.split(/\r?\n/);
71104
71085
  if (contentLines.length === 0 || contentLines.length === 1 && !contentLines[0]) {
71105
71086
  return summary;
@@ -71107,7 +71088,7 @@ var writeFileTool = {
71107
71088
  const cap = 50;
71108
71089
  const shown = contentLines.slice(0, cap);
71109
71090
  const padWidth = String(contentLines.length).length;
71110
- const detail = shown.map((l, i) => `${String(i + 1).padStart(padWidth)} ${l}`).join(`
71091
+ const detail = shown.map((l, i) => `${source_default.dim(String(i + 1).padStart(padWidth))} ${source_default.white(l)}`).join(`
71111
71092
  `);
71112
71093
  const overflow = contentLines.length > cap ? `
71113
71094
  … (${contentLines.length - cap} more lines)` : "";
@@ -71233,6 +71214,237 @@ import {
71233
71214
  writeSync as writeSync6
71234
71215
  } from "node:fs";
71235
71216
  import { dirname as dirname6, join as join8 } from "node:path";
71217
+
71218
+ // src/utils/diff.ts
71219
+ function tokenizeLines(content) {
71220
+ if (!content)
71221
+ return [];
71222
+ const tokens = [];
71223
+ const regex = /([^\r\n]*)(\r?\n)?/g;
71224
+ let match;
71225
+ while ((match = regex.exec(content)) !== null) {
71226
+ if (match.index === content.length)
71227
+ break;
71228
+ const text = match[1] ?? "";
71229
+ const ending = match[2] ?? "";
71230
+ tokens.push({ text, ending });
71231
+ if (!ending && match.index + text.length === content.length)
71232
+ break;
71233
+ }
71234
+ return tokens;
71235
+ }
71236
+ function tokenizeWords(text) {
71237
+ if (!text)
71238
+ return [];
71239
+ const matches = text.match(/[\w]+|[^\w\s]+|\s+/g);
71240
+ return matches ?? [text];
71241
+ }
71242
+ function myersDiff(oldArr, newArr, equals = (a, b) => a === b) {
71243
+ const n = oldArr.length;
71244
+ const m = newArr.length;
71245
+ if (n === 0 && m === 0)
71246
+ return [];
71247
+ if (n === 0)
71248
+ return newArr.map((newItem, i) => ({ kind: "insert", newItem, newIndex: i }));
71249
+ if (m === 0)
71250
+ return oldArr.map((oldItem, i) => ({ kind: "delete", oldItem, oldIndex: i }));
71251
+ const max = n + m;
71252
+ const v = { 1: 0 };
71253
+ const trace = [];
71254
+ for (let d = 0;d <= max; d++) {
71255
+ trace.push({ ...v });
71256
+ for (let k = -d;k <= d; k += 2) {
71257
+ let x = k === -d || k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1) ? v[k + 1] ?? 0 : (v[k - 1] ?? 0) + 1;
71258
+ let y = x - k;
71259
+ while (x < n && y < m && equals(oldArr[x], newArr[y])) {
71260
+ x++;
71261
+ y++;
71262
+ }
71263
+ v[k] = x;
71264
+ if (x >= n && y >= m) {
71265
+ const ops = [];
71266
+ let cx = n;
71267
+ let cy = m;
71268
+ for (let cd = d;cd > 0; cd--) {
71269
+ const prevV = trace[cd];
71270
+ const pk = cx - cy;
71271
+ const prevK = pk === -cd || pk !== cd && (prevV[pk - 1] ?? -1) < (prevV[pk + 1] ?? -1) ? pk + 1 : pk - 1;
71272
+ const prevX = prevV[prevK] ?? 0;
71273
+ const prevY = prevX - prevK;
71274
+ while (cx > prevX + (prevK === pk + 1 ? 0 : 1) && cy > prevY + (prevK === pk + 1 ? 1 : 0)) {
71275
+ cx--;
71276
+ cy--;
71277
+ ops.push({
71278
+ kind: "equal",
71279
+ oldItem: oldArr[cx],
71280
+ newItem: newArr[cy],
71281
+ oldIndex: cx,
71282
+ newIndex: cy
71283
+ });
71284
+ }
71285
+ if (prevK === pk + 1) {
71286
+ cy--;
71287
+ ops.push({ kind: "insert", newItem: newArr[cy], newIndex: cy });
71288
+ } else {
71289
+ cx--;
71290
+ ops.push({ kind: "delete", oldItem: oldArr[cx], oldIndex: cx });
71291
+ }
71292
+ }
71293
+ while (cx > 0 && cy > 0) {
71294
+ cx--;
71295
+ cy--;
71296
+ ops.push({
71297
+ kind: "equal",
71298
+ oldItem: oldArr[cx],
71299
+ newItem: newArr[cy],
71300
+ oldIndex: cx,
71301
+ newIndex: cy
71302
+ });
71303
+ }
71304
+ ops.reverse();
71305
+ return ops;
71306
+ }
71307
+ }
71308
+ }
71309
+ return [];
71310
+ }
71311
+ function computeWordDiff(oldLine, newLine) {
71312
+ const oldWords = tokenizeWords(oldLine);
71313
+ const newWords = tokenizeWords(newLine);
71314
+ const ops = myersDiff(oldWords, newWords);
71315
+ const oldSpans = [];
71316
+ const newSpans = [];
71317
+ for (const op of ops) {
71318
+ if (op.kind === "equal") {
71319
+ const text = op.oldItem ?? "";
71320
+ oldSpans.push({ text, kind: "equal" });
71321
+ newSpans.push({ text, kind: "equal" });
71322
+ } else if (op.kind === "delete") {
71323
+ oldSpans.push({ text: op.oldItem ?? "", kind: "deletion" });
71324
+ } else if (op.kind === "insert") {
71325
+ newSpans.push({ text: op.newItem ?? "", kind: "addition" });
71326
+ }
71327
+ }
71328
+ return { oldSpans, newSpans };
71329
+ }
71330
+ function buildUnifiedDiff(before, after, contextLines = 3) {
71331
+ const oldTokens = tokenizeLines(before);
71332
+ const newTokens = tokenizeLines(after);
71333
+ if (before === after) {
71334
+ return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
71335
+ }
71336
+ const ops = myersDiff(oldTokens, newTokens, (a, b) => a.text === b.text && a.ending === b.ending);
71337
+ const editIndices = ops.map((op, i) => op.kind !== "equal" ? i : -1).filter((i) => i !== -1);
71338
+ if (editIndices.length === 0) {
71339
+ return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
71340
+ }
71341
+ const hunkRanges = [];
71342
+ let currentRange = {
71343
+ startIdx: Math.max(0, editIndices[0] - contextLines),
71344
+ endIdx: Math.min(ops.length - 1, editIndices[0] + contextLines)
71345
+ };
71346
+ for (let i = 1;i < editIndices.length; i++) {
71347
+ const editIdx = editIndices[i];
71348
+ const opStart = Math.max(0, editIdx - contextLines);
71349
+ const opEnd = Math.min(ops.length - 1, editIdx + contextLines);
71350
+ if (opStart <= currentRange.endIdx) {
71351
+ currentRange.endIdx = Math.max(currentRange.endIdx, opEnd);
71352
+ } else {
71353
+ hunkRanges.push(currentRange);
71354
+ currentRange = { startIdx: opStart, endIdx: opEnd };
71355
+ }
71356
+ }
71357
+ hunkRanges.push(currentRange);
71358
+ const hunks = [];
71359
+ for (const range of hunkRanges) {
71360
+ const lines = [];
71361
+ let oldCount = 0;
71362
+ let newCount = 0;
71363
+ let oldStart = null;
71364
+ let newStart = null;
71365
+ for (let i = range.startIdx;i <= range.endIdx; i++) {
71366
+ const op = ops[i];
71367
+ if (op.kind === "equal") {
71368
+ const oIdx = op.oldIndex + 1;
71369
+ const nIdx = op.newIndex + 1;
71370
+ if (oldStart === null)
71371
+ oldStart = oIdx;
71372
+ if (newStart === null)
71373
+ newStart = nIdx;
71374
+ lines.push({
71375
+ kind: "context",
71376
+ text: op.oldItem.text,
71377
+ ending: op.oldItem.ending,
71378
+ oldLineNumber: oIdx,
71379
+ newLineNumber: nIdx
71380
+ });
71381
+ oldCount++;
71382
+ newCount++;
71383
+ } else if (op.kind === "delete") {
71384
+ const oIdx = op.oldIndex + 1;
71385
+ if (oldStart === null)
71386
+ oldStart = oIdx;
71387
+ lines.push({
71388
+ kind: "deletion",
71389
+ text: op.oldItem.text,
71390
+ ending: op.oldItem.ending,
71391
+ oldLineNumber: oIdx
71392
+ });
71393
+ oldCount++;
71394
+ } else if (op.kind === "insert") {
71395
+ const nIdx = op.newIndex + 1;
71396
+ if (newStart === null)
71397
+ newStart = nIdx;
71398
+ lines.push({
71399
+ kind: "addition",
71400
+ text: op.newItem.text,
71401
+ ending: op.newItem.ending,
71402
+ newLineNumber: nIdx
71403
+ });
71404
+ newCount++;
71405
+ }
71406
+ }
71407
+ let l = 0;
71408
+ while (l < lines.length) {
71409
+ if (lines[l]?.kind === "deletion") {
71410
+ const delStart = l;
71411
+ while (l < lines.length && lines[l]?.kind === "deletion")
71412
+ l++;
71413
+ const delEnd = l;
71414
+ const addStart = l;
71415
+ while (l < lines.length && lines[l]?.kind === "addition")
71416
+ l++;
71417
+ const addEnd = l;
71418
+ const delCount = delEnd - delStart;
71419
+ const addCount = addEnd - addStart;
71420
+ const pairCount = Math.min(delCount, addCount);
71421
+ for (let p = 0;p < pairCount; p++) {
71422
+ const delLine = lines[delStart + p];
71423
+ const addLine = lines[addStart + p];
71424
+ const { oldSpans, newSpans } = computeWordDiff(delLine.text, addLine.text);
71425
+ delLine.spans = oldSpans;
71426
+ addLine.spans = newSpans;
71427
+ }
71428
+ } else {
71429
+ l++;
71430
+ }
71431
+ }
71432
+ hunks.push({
71433
+ oldStart: oldStart ?? (oldTokens.length === 0 ? 0 : 1),
71434
+ oldCount,
71435
+ newStart: newStart ?? (newTokens.length === 0 ? 0 : 1),
71436
+ newCount,
71437
+ lines
71438
+ });
71439
+ }
71440
+ return {
71441
+ hunks,
71442
+ oldLineCount: oldTokens.length,
71443
+ newLineCount: newTokens.length
71444
+ };
71445
+ }
71446
+
71447
+ // src/tools/edit-file/index.ts
71236
71448
  var editFileInputSchema = object({
71237
71449
  file_path: string2().describe("The path of the file to edit (relative to current working directory or absolute inside trusted workspace)."),
71238
71450
  old_string: string2().describe("The exact string to find and replace in the file."),
@@ -71260,27 +71472,54 @@ var editFileTool = {
71260
71472
  const added = result?.addedLines ?? 0;
71261
71473
  const removed = result?.removedLines ?? 0;
71262
71474
  const summary = `Added ${added} line${added === 1 ? "" : "s"}, removed ${removed} line${removed === 1 ? "" : "s"}`;
71263
- const oldLines = args.old_string.split(/\r?\n/);
71264
- const newLines = args.new_string.split(/\r?\n/);
71265
71475
  if (added === 0 && removed === 0)
71266
71476
  return summary;
71267
- const cap = 30;
71268
- const padWidth = String(Math.max(oldLines.length, newLines.length)).length;
71477
+ const diff = buildUnifiedDiff(args.old_string, args.new_string, 2);
71269
71478
  const theme = getTheme();
71270
71479
  const deleteStyle = (str) => themeBgColor(theme.diffDeleteBG)(themeColor2(theme.diffDeleteFG)(str));
71271
71480
  const addStyle = (str) => themeBgColor(theme.diffAddBG)(themeColor2(theme.diffAddFG)(str));
71272
- const removedDetail = oldLines.slice(0, cap).map((l, i) => deleteStyle(`${String(i + 1).padStart(padWidth)} -${l}`)).join(`
71273
- `);
71274
- const addedDetail = newLines.slice(0, cap).map((l, i) => addStyle(`${String(i + 1).padStart(padWidth)} +${l}`)).join(`
71275
- `);
71276
- const overflowOld = oldLines.length > cap ? `
71277
- … (${oldLines.length - cap} more removed)` : "";
71278
- const overflowNew = newLines.length > cap ? `
71279
- … (${newLines.length - cap} more added)` : "";
71280
- const diffBlock = [removedDetail + overflowOld, addedDetail + overflowNew].filter(Boolean).join(`
71281
- `);
71282
- return `${summary}
71283
- ${diffBlock}`;
71481
+ const contextStyle = (str) => source_default.dim(str);
71482
+ let maxLineNum = 1;
71483
+ for (const hunk of diff.hunks) {
71484
+ for (const line of hunk.lines) {
71485
+ if (line.oldLineNumber)
71486
+ maxLineNum = Math.max(maxLineNum, line.oldLineNumber);
71487
+ if (line.newLineNumber)
71488
+ maxLineNum = Math.max(maxLineNum, line.newLineNumber);
71489
+ }
71490
+ }
71491
+ const padWidth = Math.max(1, String(maxLineNum).length);
71492
+ const diffLines = [];
71493
+ const cap = 30;
71494
+ let count = 0;
71495
+ let overflow = 0;
71496
+ for (const hunk of diff.hunks) {
71497
+ for (const line of hunk.lines) {
71498
+ if (count >= cap) {
71499
+ overflow++;
71500
+ continue;
71501
+ }
71502
+ count++;
71503
+ if (line.kind === "deletion") {
71504
+ const numStr = String(line.oldLineNumber ?? "").padStart(padWidth, " ");
71505
+ const body = line.spans ? "-" + line.spans.map((s) => s.kind === "deletion" ? source_default.bold(s.text) : s.text).join("") : `-${line.text}`;
71506
+ diffLines.push(deleteStyle(`${numStr} ${body}`));
71507
+ } else if (line.kind === "addition") {
71508
+ const numStr = String(line.newLineNumber ?? "").padStart(padWidth, " ");
71509
+ const body = line.spans ? "+" + line.spans.map((s) => s.kind === "addition" ? source_default.bold(s.text) : s.text).join("") : `+${line.text}`;
71510
+ diffLines.push(addStyle(`${numStr} ${body}`));
71511
+ } else {
71512
+ const numStr = String(line.newLineNumber ?? line.oldLineNumber ?? "").padStart(padWidth, " ");
71513
+ diffLines.push(contextStyle(`${numStr} ${line.text}`));
71514
+ }
71515
+ }
71516
+ }
71517
+ if (overflow > 0) {
71518
+ diffLines.push(source_default.dim(` … (${overflow} more lines)`));
71519
+ }
71520
+ return diffLines.length > 0 ? `${summary}
71521
+ ${diffLines.join(`
71522
+ `)}` : summary;
71284
71523
  },
71285
71524
  execute: async (args, context) => {
71286
71525
  const { absolutePath: targetPath, relativePath } = resolveDirectMutationPath(context.cwd, args.file_path);
@@ -72440,7 +72679,7 @@ var taskKillTool = {
72440
72679
  // package.json
72441
72680
  var package_default = {
72442
72681
  name: "steward-cli",
72443
- version: "0.11.0",
72682
+ version: "0.12.0",
72444
72683
  description: "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
72445
72684
  author: "sapirrior",
72446
72685
  license: "MIT",
@@ -118861,6 +119100,9 @@ class StatusBar extends Component {
118861
119100
  });
118862
119101
  }
118863
119102
  }
119103
+ setUpdateStatus(updateStatus) {
119104
+ this.setState({ updateStatus });
119105
+ }
118864
119106
  showWarning(warningText, durationMs = 4000) {
118865
119107
  if (this.warningTimer) {
118866
119108
  clearTimeout(this.warningTimer);
@@ -118888,7 +119130,16 @@ class StatusBar extends Component {
118888
119130
  const theme = getTheme();
118889
119131
  const termWidth = width ?? process.stdout.columns ?? 80;
118890
119132
  const maxCols = Math.max(1, termWidth);
118891
- const { model, usage, isBusy, exitPending, warning, voiceState, voiceDurationSec } = this.state;
119133
+ const {
119134
+ model,
119135
+ usage,
119136
+ isBusy,
119137
+ exitPending,
119138
+ warning,
119139
+ voiceState,
119140
+ voiceDurationSec,
119141
+ updateStatus
119142
+ } = this.state;
118892
119143
  let left = "";
118893
119144
  if (warning) {
118894
119145
  left = themeColor2(theme.warning)(warning);
@@ -118904,6 +119155,24 @@ class StatusBar extends Component {
118904
119155
  } else if (exitPending) {
118905
119156
  const errColor = themeColor2(theme.error);
118906
119157
  left = `${errColor("▸ ")}${source_default.dim("Press ")}${errColor("Ctrl+C")}${source_default.dim(" again to exit")}`;
119158
+ } else if (updateStatus && updateStatus.state !== "idle") {
119159
+ if (updateStatus.state === "checking") {
119160
+ 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
+ } 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");
119169
+ } else if (updateStatus.state === "error" && updateStatus.message) {
119170
+ left = themeColor2(theme.error)(updateStatus.message);
119171
+ } else if (isBusy) {
119172
+ left = source_default.dim("esc to interrupt");
119173
+ } else {
119174
+ left = source_default.dim("? for shortcuts");
119175
+ }
118907
119176
  } else if (isBusy) {
118908
119177
  left = source_default.dim("esc to interrupt");
118909
119178
  } else {
@@ -120545,235 +120814,6 @@ class BashPermissionDock extends Component {
120545
120814
  }
120546
120815
  }
120547
120816
 
120548
- // src/utils/diff.ts
120549
- function tokenizeLines(content) {
120550
- if (!content)
120551
- return [];
120552
- const tokens = [];
120553
- const regex = /([^\r\n]*)(\r?\n)?/g;
120554
- let match;
120555
- while ((match = regex.exec(content)) !== null) {
120556
- if (match.index === content.length)
120557
- break;
120558
- const text = match[1] ?? "";
120559
- const ending = match[2] ?? "";
120560
- tokens.push({ text, ending });
120561
- if (!ending && match.index + text.length === content.length)
120562
- break;
120563
- }
120564
- return tokens;
120565
- }
120566
- function tokenizeWords(text) {
120567
- if (!text)
120568
- return [];
120569
- const matches = text.match(/[\w]+|[^\w\s]+|\s+/g);
120570
- return matches ?? [text];
120571
- }
120572
- function myersDiff(oldArr, newArr, equals = (a, b) => a === b) {
120573
- const n = oldArr.length;
120574
- const m = newArr.length;
120575
- if (n === 0 && m === 0)
120576
- return [];
120577
- if (n === 0)
120578
- return newArr.map((newItem, i) => ({ kind: "insert", newItem, newIndex: i }));
120579
- if (m === 0)
120580
- return oldArr.map((oldItem, i) => ({ kind: "delete", oldItem, oldIndex: i }));
120581
- const max = n + m;
120582
- const v = { 1: 0 };
120583
- const trace = [];
120584
- for (let d = 0;d <= max; d++) {
120585
- trace.push({ ...v });
120586
- for (let k = -d;k <= d; k += 2) {
120587
- let x = k === -d || k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1) ? v[k + 1] ?? 0 : (v[k - 1] ?? 0) + 1;
120588
- let y = x - k;
120589
- while (x < n && y < m && equals(oldArr[x], newArr[y])) {
120590
- x++;
120591
- y++;
120592
- }
120593
- v[k] = x;
120594
- if (x >= n && y >= m) {
120595
- const ops = [];
120596
- let cx = n;
120597
- let cy = m;
120598
- for (let cd = d;cd > 0; cd--) {
120599
- const prevV = trace[cd];
120600
- const pk = cx - cy;
120601
- const prevK = pk === -cd || pk !== cd && (prevV[pk - 1] ?? -1) < (prevV[pk + 1] ?? -1) ? pk + 1 : pk - 1;
120602
- const prevX = prevV[prevK] ?? 0;
120603
- const prevY = prevX - prevK;
120604
- while (cx > prevX + (prevK === pk + 1 ? 0 : 1) && cy > prevY + (prevK === pk + 1 ? 1 : 0)) {
120605
- cx--;
120606
- cy--;
120607
- ops.push({
120608
- kind: "equal",
120609
- oldItem: oldArr[cx],
120610
- newItem: newArr[cy],
120611
- oldIndex: cx,
120612
- newIndex: cy
120613
- });
120614
- }
120615
- if (prevK === pk + 1) {
120616
- cy--;
120617
- ops.push({ kind: "insert", newItem: newArr[cy], newIndex: cy });
120618
- } else {
120619
- cx--;
120620
- ops.push({ kind: "delete", oldItem: oldArr[cx], oldIndex: cx });
120621
- }
120622
- }
120623
- while (cx > 0 && cy > 0) {
120624
- cx--;
120625
- cy--;
120626
- ops.push({
120627
- kind: "equal",
120628
- oldItem: oldArr[cx],
120629
- newItem: newArr[cy],
120630
- oldIndex: cx,
120631
- newIndex: cy
120632
- });
120633
- }
120634
- ops.reverse();
120635
- return ops;
120636
- }
120637
- }
120638
- }
120639
- return [];
120640
- }
120641
- function computeWordDiff(oldLine, newLine) {
120642
- const oldWords = tokenizeWords(oldLine);
120643
- const newWords = tokenizeWords(newLine);
120644
- const ops = myersDiff(oldWords, newWords);
120645
- const oldSpans = [];
120646
- const newSpans = [];
120647
- for (const op of ops) {
120648
- if (op.kind === "equal") {
120649
- const text = op.oldItem ?? "";
120650
- oldSpans.push({ text, kind: "equal" });
120651
- newSpans.push({ text, kind: "equal" });
120652
- } else if (op.kind === "delete") {
120653
- oldSpans.push({ text: op.oldItem ?? "", kind: "deletion" });
120654
- } else if (op.kind === "insert") {
120655
- newSpans.push({ text: op.newItem ?? "", kind: "addition" });
120656
- }
120657
- }
120658
- return { oldSpans, newSpans };
120659
- }
120660
- function buildUnifiedDiff(before, after, contextLines = 3) {
120661
- const oldTokens = tokenizeLines(before);
120662
- const newTokens = tokenizeLines(after);
120663
- if (before === after) {
120664
- return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
120665
- }
120666
- const ops = myersDiff(oldTokens, newTokens, (a, b) => a.text === b.text && a.ending === b.ending);
120667
- const editIndices = ops.map((op, i) => op.kind !== "equal" ? i : -1).filter((i) => i !== -1);
120668
- if (editIndices.length === 0) {
120669
- return { hunks: [], oldLineCount: oldTokens.length, newLineCount: newTokens.length };
120670
- }
120671
- const hunkRanges = [];
120672
- let currentRange = {
120673
- startIdx: Math.max(0, editIndices[0] - contextLines),
120674
- endIdx: Math.min(ops.length - 1, editIndices[0] + contextLines)
120675
- };
120676
- for (let i = 1;i < editIndices.length; i++) {
120677
- const editIdx = editIndices[i];
120678
- const opStart = Math.max(0, editIdx - contextLines);
120679
- const opEnd = Math.min(ops.length - 1, editIdx + contextLines);
120680
- if (opStart <= currentRange.endIdx) {
120681
- currentRange.endIdx = Math.max(currentRange.endIdx, opEnd);
120682
- } else {
120683
- hunkRanges.push(currentRange);
120684
- currentRange = { startIdx: opStart, endIdx: opEnd };
120685
- }
120686
- }
120687
- hunkRanges.push(currentRange);
120688
- const hunks = [];
120689
- for (const range of hunkRanges) {
120690
- const lines = [];
120691
- let oldCount = 0;
120692
- let newCount = 0;
120693
- let oldStart = null;
120694
- let newStart = null;
120695
- for (let i = range.startIdx;i <= range.endIdx; i++) {
120696
- const op = ops[i];
120697
- if (op.kind === "equal") {
120698
- const oIdx = op.oldIndex + 1;
120699
- const nIdx = op.newIndex + 1;
120700
- if (oldStart === null)
120701
- oldStart = oIdx;
120702
- if (newStart === null)
120703
- newStart = nIdx;
120704
- lines.push({
120705
- kind: "context",
120706
- text: op.oldItem.text,
120707
- ending: op.oldItem.ending,
120708
- oldLineNumber: oIdx,
120709
- newLineNumber: nIdx
120710
- });
120711
- oldCount++;
120712
- newCount++;
120713
- } else if (op.kind === "delete") {
120714
- const oIdx = op.oldIndex + 1;
120715
- if (oldStart === null)
120716
- oldStart = oIdx;
120717
- lines.push({
120718
- kind: "deletion",
120719
- text: op.oldItem.text,
120720
- ending: op.oldItem.ending,
120721
- oldLineNumber: oIdx
120722
- });
120723
- oldCount++;
120724
- } else if (op.kind === "insert") {
120725
- const nIdx = op.newIndex + 1;
120726
- if (newStart === null)
120727
- newStart = nIdx;
120728
- lines.push({
120729
- kind: "addition",
120730
- text: op.newItem.text,
120731
- ending: op.newItem.ending,
120732
- newLineNumber: nIdx
120733
- });
120734
- newCount++;
120735
- }
120736
- }
120737
- let l = 0;
120738
- while (l < lines.length) {
120739
- if (lines[l]?.kind === "deletion") {
120740
- const delStart = l;
120741
- while (l < lines.length && lines[l]?.kind === "deletion")
120742
- l++;
120743
- const delEnd = l;
120744
- const addStart = l;
120745
- while (l < lines.length && lines[l]?.kind === "addition")
120746
- l++;
120747
- const addEnd = l;
120748
- const delCount = delEnd - delStart;
120749
- const addCount = addEnd - addStart;
120750
- const pairCount = Math.min(delCount, addCount);
120751
- for (let p = 0;p < pairCount; p++) {
120752
- const delLine = lines[delStart + p];
120753
- const addLine = lines[addStart + p];
120754
- const { oldSpans, newSpans } = computeWordDiff(delLine.text, addLine.text);
120755
- delLine.spans = oldSpans;
120756
- addLine.spans = newSpans;
120757
- }
120758
- } else {
120759
- l++;
120760
- }
120761
- }
120762
- hunks.push({
120763
- oldStart: oldStart ?? (oldTokens.length === 0 ? 0 : 1),
120764
- oldCount,
120765
- newStart: newStart ?? (newTokens.length === 0 ? 0 : 1),
120766
- newCount,
120767
- lines
120768
- });
120769
- }
120770
- return {
120771
- hunks,
120772
- oldLineCount: oldTokens.length,
120773
- newLineCount: newTokens.length
120774
- };
120775
- }
120776
-
120777
120817
  // src/tui/components/docks/FilePermissionDock.tsx
120778
120818
  class FilePermissionDock extends Component {
120779
120819
  wrap = true;
@@ -120903,8 +120943,7 @@ class FilePermissionDock extends Component {
120903
120943
  const wrapped = prefixedBlock(firstPrefix, bodyText, {
120904
120944
  continuationPrefix: contPrefix,
120905
120945
  width: maxCols,
120906
- bg: diffDeleteBg,
120907
- padToWidth: true
120946
+ bg: diffDeleteBg
120908
120947
  });
120909
120948
  renderedRows.push(...wrapped);
120910
120949
  } else if (line.kind === "addition") {
@@ -120915,8 +120954,7 @@ class FilePermissionDock extends Component {
120915
120954
  const wrapped = prefixedBlock(firstPrefix, bodyText, {
120916
120955
  continuationPrefix: contPrefix,
120917
120956
  width: maxCols,
120918
- bg: diffAddBg,
120919
- padToWidth: true
120957
+ bg: diffAddBg
120920
120958
  });
120921
120959
  renderedRows.push(...wrapped);
120922
120960
  } else {
@@ -122049,6 +122087,230 @@ class VoiceController {
122049
122087
  this.setState("idle");
122050
122088
  }
122051
122089
  }
122090
+ // src/services/updater/service.ts
122091
+ import { spawn as spawn2 } from "node:child_process";
122092
+ var DEFAULT_REPO = "sapirrior/steward";
122093
+ function parseSemver(v) {
122094
+ const clean = v.replace(/^v/i, "").trim().split("-")[0] ?? "";
122095
+ const parts = clean.split(".").map((p) => parseInt(p, 10) || 0);
122096
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
122097
+ }
122098
+ function isNewerVersion(current, latest) {
122099
+ const [cMaj, cMin, cPat] = parseSemver(current);
122100
+ const [lMaj, lMin, lPat] = parseSemver(latest);
122101
+ if (lMaj > cMaj)
122102
+ return true;
122103
+ if (lMaj < cMaj)
122104
+ return false;
122105
+ if (lMin > cMin)
122106
+ return true;
122107
+ if (lMin < cMin)
122108
+ return false;
122109
+ return lPat > cPat;
122110
+ }
122111
+
122112
+ class AutoUpdaterService {
122113
+ currentVersion;
122114
+ repo;
122115
+ state = "idle";
122116
+ options;
122117
+ checkTimer = null;
122118
+ intervalTimer = null;
122119
+ isUpdating = false;
122120
+ constructor(options = {}) {
122121
+ this.options = options;
122122
+ this.currentVersion = options.currentVersion || package_default.version || "0.0.0";
122123
+ this.repo = options.repo || DEFAULT_REPO;
122124
+ }
122125
+ getState() {
122126
+ return this.state;
122127
+ }
122128
+ getRepo() {
122129
+ return this.repo;
122130
+ }
122131
+ getCurrentVersion() {
122132
+ return this.currentVersion;
122133
+ }
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`;
122142
+ }
122143
+ setState(state, info) {
122144
+ this.state = state;
122145
+ this.options.onStatusChange?.(state, info);
122146
+ }
122147
+ async checkForUpdates() {
122148
+ if (process.env.STEWARD_NO_UPDATE_CHECK === "1" || process.env.STEWARD_NO_UPDATE_CHECK === "true") {
122149
+ return null;
122150
+ }
122151
+ try {
122152
+ this.setState("checking", { message: "Checking for updates..." });
122153
+ const controller = new AbortController;
122154
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 1e4);
122155
+ const response = await fetch(this.latestReleaseApi, {
122156
+ headers: {
122157
+ "User-Agent": `steward-cli/${this.currentVersion}`,
122158
+ Accept: "application/vnd.github.v3+json"
122159
+ },
122160
+ signal: controller.signal
122161
+ });
122162
+ clearTimeout(timeout);
122163
+ if (!response.ok) {
122164
+ this.setState("idle");
122165
+ return null;
122166
+ }
122167
+ const data = await response.json();
122168
+ const rawTag = data.tag_name ?? "";
122169
+ const latestVersion = rawTag.replace(/^v/i, "").trim();
122170
+ if (!latestVersion) {
122171
+ this.setState("idle");
122172
+ return null;
122173
+ }
122174
+ const hasUpdate = isNewerVersion(this.currentVersion, latestVersion);
122175
+ const updateInfo = {
122176
+ currentVersion: this.currentVersion,
122177
+ latestVersion,
122178
+ hasUpdate,
122179
+ tag: rawTag,
122180
+ releaseUrl: data.html_url
122181
+ };
122182
+ if (hasUpdate) {
122183
+ this.setState("available", {
122184
+ 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
122223
+ });
122224
+ } 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
+ }
122232
+ });
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") {
122263
+ this.setState("idle");
122264
+ }
122265
+ }, 4000);
122266
+ return false;
122267
+ }
122268
+ } catch (err) {
122269
+ logError(err, { component: "AutoUpdaterService", method: "installUpdate" });
122270
+ this.setState("error", { version, message: "Update failed" });
122271
+ setTimeout(() => {
122272
+ if (this.state === "error") {
122273
+ this.setState("idle");
122274
+ }
122275
+ }, 4000);
122276
+ return false;
122277
+ } finally {
122278
+ this.isUpdating = false;
122279
+ }
122280
+ }
122281
+ startBackgroundCheck(initialDelayMs = 2000, intervalMs) {
122282
+ if (this.checkTimer)
122283
+ 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
+ };
122294
+ this.checkTimer = setTimeout(async () => {
122295
+ 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
+ }
122301
+ }, initialDelayMs);
122302
+ }
122303
+ dispose() {
122304
+ if (this.checkTimer) {
122305
+ clearTimeout(this.checkTimer);
122306
+ this.checkTimer = null;
122307
+ }
122308
+ if (this.intervalTimer) {
122309
+ clearInterval(this.intervalTimer);
122310
+ this.intervalTimer = null;
122311
+ }
122312
+ }
122313
+ }
122052
122314
  // src/tui/app.ts
122053
122315
  class TUIApp {
122054
122316
  engine;
@@ -122064,6 +122326,7 @@ class TUIApp {
122064
122326
  ctrlCTimer = null;
122065
122327
  isBusy = false;
122066
122328
  voiceController;
122329
+ autoUpdater;
122067
122330
  permissionQueue;
122068
122331
  constructor(options = {}) {
122069
122332
  this.cwd = options.cwd ?? process.cwd();
@@ -122126,6 +122389,20 @@ class TUIApp {
122126
122389
  this.statusBar.showWarning(warning);
122127
122390
  }
122128
122391
  });
122392
+ this.autoUpdater = new AutoUpdaterService({
122393
+ currentVersion: options.version,
122394
+ onStatusChange: (state, info) => {
122395
+ if (state === "idle") {
122396
+ this.statusBar.setUpdateStatus(undefined);
122397
+ } else {
122398
+ this.statusBar.setUpdateStatus({
122399
+ state,
122400
+ version: info?.version,
122401
+ message: info?.message
122402
+ });
122403
+ }
122404
+ }
122405
+ });
122129
122406
  this.permissionQueue = new PermissionQueue({
122130
122407
  onShow: (item) => {
122131
122408
  if (this.activeModal)
@@ -122222,6 +122499,7 @@ class TUIApp {
122222
122499
  this.engine.mount(this.streamingView, { kind: "custom" });
122223
122500
  this.engine.mount(this.promptInput, { keepCursorVisible: true, kind: "input" });
122224
122501
  this.engine.mount(this.statusBar, { kind: "custom" });
122502
+ this.autoUpdater.startBackgroundCheck(3000);
122225
122503
  this.engine.addInputListener((chunk) => {
122226
122504
  const action = parseKeyInput(chunk);
122227
122505
  if (this.voiceController.isActive) {
@@ -122657,6 +122935,7 @@ class TUIApp {
122657
122935
  this.permissionQueue.clear();
122658
122936
  this.session.shutdown().catch(() => {});
122659
122937
  this.voiceController.dispose();
122938
+ this.autoUpdater.dispose();
122660
122939
  this.engine.cleanupSync();
122661
122940
  if (this.onExitCallback) {
122662
122941
  this.onExitCallback();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steward-cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Interactive AI engineering assistant for the terminal with human-in-the-loop control across local and frontier models.",
5
5
  "author": "sapirrior",
6
6
  "license": "MIT",