zidane 3.1.2 → 3.3.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.
@@ -5,10 +5,10 @@ import {
5
5
  interpolateShellCommands,
6
6
  resolveSkills,
7
7
  validateResourcePath
8
- } from "./chunk-TPXPVEH6.js";
8
+ } from "./chunk-X3VOTPVM.js";
9
9
  import {
10
10
  createProcessContext
11
- } from "./chunk-IUBBVF53.js";
11
+ } from "./chunk-UD25QF3H.js";
12
12
  import {
13
13
  connectMcpServers
14
14
  } from "./chunk-7H34OFDA.js";
@@ -69,6 +69,26 @@ function desanitize(s) {
69
69
  out = out.replaceAll(from, to);
70
70
  return out;
71
71
  }
72
+ var LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
73
+ function stripLineNumberPrefixes(s) {
74
+ return s.replace(LINE_NUMBER_PREFIX_RE, "");
75
+ }
76
+ function locateAndCount(haystack, normFile, target, via) {
77
+ const idx = normFile.indexOf(target);
78
+ if (idx === -1)
79
+ return null;
80
+ const actual = haystack.slice(idx, idx + target.length);
81
+ let occ = 0;
82
+ let cursor = 0;
83
+ while (true) {
84
+ const next = normFile.indexOf(target, cursor);
85
+ if (next === -1)
86
+ break;
87
+ occ++;
88
+ cursor = next + target.length;
89
+ }
90
+ return { actual, occurrences: occ, via };
91
+ }
72
92
  function resolveOldString(haystack, needle) {
73
93
  const exact = countExactMatches(haystack, needle);
74
94
  if (exact > 0)
@@ -76,20 +96,9 @@ function resolveOldString(haystack, needle) {
76
96
  const normNeedle = normalizeQuotes(needle);
77
97
  const normFile = normalizeQuotes(haystack);
78
98
  if (normNeedle !== needle || normFile !== haystack) {
79
- const idx = normFile.indexOf(normNeedle);
80
- if (idx !== -1) {
81
- const actual = haystack.slice(idx, idx + normNeedle.length);
82
- let occ = 0;
83
- let cursor = 0;
84
- while (true) {
85
- const next = normFile.indexOf(normNeedle, cursor);
86
- if (next === -1)
87
- break;
88
- occ++;
89
- cursor = next + normNeedle.length;
90
- }
91
- return { actual, occurrences: occ, via: "quotes" };
92
- }
99
+ const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
100
+ if (m)
101
+ return m;
93
102
  }
94
103
  const desan = desanitize(needle);
95
104
  if (desan !== needle) {
@@ -99,23 +108,34 @@ function resolveOldString(haystack, needle) {
99
108
  }
100
109
  const combo = desanitize(normNeedle);
101
110
  if (combo !== needle) {
102
- const idx = normFile.indexOf(combo);
103
- if (idx !== -1) {
104
- const actual = haystack.slice(idx, idx + combo.length);
105
- let occ = 0;
106
- let cursor = 0;
107
- while (true) {
108
- const next = normFile.indexOf(combo, cursor);
109
- if (next === -1)
110
- break;
111
- occ++;
112
- cursor = next + combo.length;
113
- }
114
- return { actual, occurrences: occ, via: "quotes+desanitize" };
111
+ const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
112
+ if (m)
113
+ return m;
114
+ }
115
+ const stripped = stripLineNumberPrefixes(needle);
116
+ if (stripped !== needle && stripped.trim().length > 0) {
117
+ const count = countExactMatches(haystack, stripped);
118
+ if (count > 0)
119
+ return { actual: stripped, occurrences: count, via: "line-numbers" };
120
+ const strippedNorm = normalizeQuotes(stripped);
121
+ if (strippedNorm !== stripped || normFile !== haystack) {
122
+ const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
123
+ if (m)
124
+ return m;
115
125
  }
116
126
  }
117
127
  return null;
118
128
  }
129
+ function styleReplacementForVia(replacement, via, actual) {
130
+ let out = replacement;
131
+ if (via === "desanitize" || via === "quotes+desanitize")
132
+ out = desanitize(out);
133
+ if (via === "line-numbers" || via === "quotes+line-numbers")
134
+ out = stripLineNumberPrefixes(out);
135
+ if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers")
136
+ out = preserveQuoteStyle(actual, out);
137
+ return out;
138
+ }
119
139
  function preserveQuoteStyle(actual, replacement) {
120
140
  const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE);
121
141
  const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE);
@@ -221,7 +241,7 @@ function hashContent(text) {
221
241
  var edit = {
222
242
  spec: {
223
243
  name: "edit",
224
- description: "Replace exact `old_string` with `new_string` in a file. Fails if `old_string` is not unique unless `replace_all: true`. Prefer over `write_file` for surgical changes \u2014 preserves the rest of the file.",
244
+ description: "Replace exact `old_string` with `new_string` in a file. Fails if `old_string` is not unique unless `replace_all: true`. Prefer over `write_file` for surgical changes \u2014 preserves the rest of the file. Tolerates `read_file` line-number prefixes (`<N>\\t\u2026`, `<N>|\u2026`, or `<N>\u2192\u2026`) in `old_string` / `new_string` \u2014 they are stripped before matching/writing, so you can paste a numbered chunk verbatim.",
225
245
  inputSchema: {
226
246
  type: "object",
227
247
  properties: {
@@ -266,9 +286,7 @@ var edit = {
266
286
  const { actual, occurrences, via } = match;
267
287
  if (occurrences > 1 && !replaceAll)
268
288
  return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
269
- let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
270
- if (via === "quotes" || via === "quotes+desanitize")
271
- styledReplacement = preserveQuoteStyle(actual, styledReplacement);
289
+ const styledReplacement = styleReplacementForVia(replacement, via, actual);
272
290
  const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
273
291
  if (updated === original)
274
292
  return `Edit error: replacement produced no change in ${target}.`;
@@ -285,7 +303,8 @@ var edit = {
285
303
  }
286
304
  };
287
305
  function nearestMatchPreview(haystack, needle) {
288
- const needleFirstLine = needle.split("\n")[0];
306
+ const normalizedNeedle = stripLineNumberPrefixes(needle);
307
+ const needleFirstLine = normalizedNeedle.split("\n")[0];
289
308
  if (needleFirstLine.length < 3)
290
309
  return null;
291
310
  const lines = haystack.split("\n");
@@ -312,6 +331,8 @@ function sharedPrefixLength(a, b) {
312
331
  }
313
332
 
314
333
  // src/tools/glob.ts
334
+ import { stat } from "fs/promises";
335
+ import { resolve } from "path";
315
336
  var DEFAULT_LIMIT = 1e3;
316
337
  var SAFE_GLOB_PATTERN_RE = /^[\w./*?[\]{}!,^@+-]+$/;
317
338
  async function globInProcess(pattern, cwd, limit) {
@@ -338,7 +359,7 @@ async function globViaShell(pattern, ctx, limit) {
338
359
  var glob = {
339
360
  spec: {
340
361
  name: "glob",
341
- description: "Match files by glob pattern (supports **, *, ?). Relative to the execution context cwd. Returns a newline-separated list of matching paths, sorted.",
362
+ description: "Match files by glob pattern (supports **, *, ?). Relative to the execution context cwd. By default each row is `<path>\\t<size-bytes>\\t<mtime-iso>`; set `metadata: false` for a plain newline-separated list of paths. Always sorted.",
342
363
  inputSchema: {
343
364
  type: "object",
344
365
  properties: {
@@ -349,17 +370,34 @@ var glob = {
349
370
  limit: {
350
371
  type: "number",
351
372
  description: `Maximum number of matches to return. Default: ${DEFAULT_LIMIT}.`
373
+ },
374
+ metadata: {
375
+ type: "boolean",
376
+ description: "Append size (bytes) and mtime (ISO) per row, tab-separated. Default: true. In-process only \u2014 non-process execution contexts always return paths."
352
377
  }
353
378
  },
354
379
  required: ["pattern"]
355
380
  }
356
381
  },
357
- async execute({ pattern, limit }, ctx) {
382
+ async execute({ pattern, limit, metadata }, ctx) {
358
383
  const pat = pattern;
359
384
  const max = typeof limit === "number" && limit > 0 ? limit : DEFAULT_LIMIT;
385
+ const wantMetadata = metadata !== false;
360
386
  try {
361
387
  const entries = ctx.execution.type === "process" ? await globInProcess(pat, ctx.handle.cwd, max) : await globViaShell(pat, ctx, max);
362
- return entries.length > 0 ? entries.join("\n") : "(no matches)";
388
+ if (entries.length === 0)
389
+ return "(no matches)";
390
+ if (!wantMetadata || ctx.execution.type !== "process")
391
+ return entries.join("\n");
392
+ const rows = await Promise.all(entries.map(async (rel) => {
393
+ try {
394
+ const s = await stat(resolve(ctx.handle.cwd, rel));
395
+ return `${rel} ${s.size} ${new Date(s.mtimeMs).toISOString()}`;
396
+ } catch {
397
+ return `${rel} `;
398
+ }
399
+ }));
400
+ return rows.join("\n");
363
401
  } catch (err) {
364
402
  const message = err instanceof Error ? err.message : String(err);
365
403
  return `Glob error: ${message}`;
@@ -367,6 +405,18 @@ var glob = {
367
405
  }
368
406
  };
369
407
 
408
+ // src/tools/shell-quote.ts
409
+ var SAFE_TOKEN_RE = /^[\w@%+=:,./-]+$/;
410
+ var SINGLE_QUOTE_RE = /'/g;
411
+ function shellQuote(arg) {
412
+ if (SAFE_TOKEN_RE.test(arg))
413
+ return arg;
414
+ return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
415
+ }
416
+ function alwaysQuote(arg) {
417
+ return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
418
+ }
419
+
370
420
  // src/tools/grep.ts
371
421
  var DEFAULT_HEAD_LIMIT = 250;
372
422
  var DEFAULT_OUTPUT_MODE = "files_with_matches";
@@ -442,11 +492,6 @@ async function runViaRipgrep(input, ctx) {
442
492
  }
443
493
  return formatPaginated(result.stdout, input);
444
494
  }
445
- function shellQuote(arg) {
446
- if (/^[\w@%+=:,./-]+$/.test(arg))
447
- return arg;
448
- return `'${arg.replace(/'/g, "'\\''")}'`;
449
- }
450
495
  async function runInProcess(input, ctx) {
451
496
  const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
452
497
  const flags = `${input["-i"] ? "i" : ""}${input.multiline ? "s" : ""}${mode !== "content" ? "" : "g"}`;
@@ -531,8 +576,8 @@ async function enumerateFiles(input, ctx) {
531
576
  const root = input.path ?? ".";
532
577
  if (input.path && !input.path.includes("*") && !input.path.includes("?")) {
533
578
  try {
534
- const stat = await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`);
535
- if (stat.stdout.trim() === "file")
579
+ const stat2 = await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`);
580
+ if (stat2.stdout.trim() === "file")
536
581
  return [input.path];
537
582
  } catch {
538
583
  }
@@ -612,7 +657,7 @@ var listFiles = {
612
657
  var multiEdit = {
613
658
  spec: {
614
659
  name: "multi_edit",
615
- description: "Apply a sequential list of edits to a file atomically. Each edit operates on the result of the previous edit. All edits must succeed for any to be written. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file.",
660
+ description: "Apply a sequential list of edits to a file atomically. Each edit operates on the result of the previous edit. All edits must succeed for any to be written. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file. Each step tolerates `read_file` line-number prefixes (`<N>\\t\u2026`, `<N>|\u2026`, or `<N>\u2192\u2026`) in `old_string` / `new_string`.",
616
661
  inputSchema: {
617
662
  type: "object",
618
663
  properties: {
@@ -673,9 +718,7 @@ var multiEdit = {
673
718
  const { actual, occurrences, via } = match;
674
719
  if (occurrences > 1 && !replaceAll)
675
720
  return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
676
- let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
677
- if (via === "quotes" || via === "quotes+desanitize")
678
- styledReplacement = preserveQuoteStyle(actual, styledReplacement);
721
+ const styledReplacement = styleReplacementForVia(replacement, via, actual);
679
722
  current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
680
723
  applied += occurrences;
681
724
  }
@@ -695,6 +738,33 @@ var multiEdit = {
695
738
  // src/tools/read-file.ts
696
739
  import { Buffer as Buffer2 } from "buffer";
697
740
 
741
+ // src/tools/binary-detect.ts
742
+ var SNIFF_BYTES = 8192;
743
+ var REPLACEMENT_RATIO_THRESHOLD = 0.01;
744
+ var REPLACEMENT_MIN_COUNT = 5;
745
+ function containsNullByte(text, sniffBytes = SNIFF_BYTES) {
746
+ const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
747
+ for (let i = 0; i < sample.length; i++) {
748
+ if (sample.charCodeAt(i) === 0)
749
+ return true;
750
+ }
751
+ return false;
752
+ }
753
+ function looksBinary(text, sniffBytes = SNIFF_BYTES) {
754
+ const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
755
+ if (sample.length === 0)
756
+ return false;
757
+ let replacementCount = 0;
758
+ for (let i = 0; i < sample.length; i++) {
759
+ const code = sample.charCodeAt(i);
760
+ if (code === 0)
761
+ return true;
762
+ if (code === 65533)
763
+ replacementCount++;
764
+ }
765
+ return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
766
+ }
767
+
698
768
  // src/tools/binary-read.ts
699
769
  import { Buffer } from "buffer";
700
770
  function imageMediaTypeFor(path) {
@@ -722,38 +792,45 @@ async function readFileAsBase64(execution, handle, path) {
722
792
  const b642 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
723
793
  return { base64: b642, byteLength: bytes.byteLength };
724
794
  }
725
- const cmd = `base64 < ${shellQuote2(path)}`;
795
+ const cmd = `base64 < ${alwaysQuote(path)}`;
726
796
  const result = await execution.exec(handle, cmd);
727
797
  if (result.exitCode !== 0)
728
798
  throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
729
799
  const b64 = result.stdout.replace(/\s+/g, "");
730
- return { base64: b64, byteLength: Math.floor(b64.length * 3 / 4) };
800
+ return { base64: b64, byteLength: decodedBase64ByteLength(b64) };
731
801
  }
732
- function shellQuote2(s) {
733
- return `'${s.replace(/'/g, "'\\''")}'`;
802
+ function decodedBase64ByteLength(b64) {
803
+ if (b64.length === 0)
804
+ return 0;
805
+ let pad = 0;
806
+ if (b64.endsWith("=="))
807
+ pad = 2;
808
+ else if (b64.endsWith("="))
809
+ pad = 1;
810
+ return Math.max(0, b64.length * 3 / 4 - pad);
734
811
  }
735
812
 
736
813
  // src/tools/read-file.ts
737
814
  var DEFAULT_LINE_LIMIT = 2e3;
738
815
  var DEFAULT_BYTE_CAP = 65536;
739
- var BINARY_PROBE_BYTES = 8e3;
740
816
  var DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
741
817
  var readFile = {
742
818
  spec: {
743
819
  name: "read_file",
744
- description: "Read a file by path. Returns lines [offset..offset+limit). Default offset=1, limit=2000. A trailing footer explains how to read the rest when truncated. Binary files return a short marker rather than mojibake.",
820
+ description: "Read a file by path. Returns lines [offset..offset+limit). Default offset=1, limit=2000. Each line is prefixed with its 1-indexed line number followed by a tab (e.g. `42\\tconst foo = bar`); the prefix is metadata, not part of the file. Mirrors Claude Code's `cat -n`-style compact output for token efficiency. A trailing footer explains how to read the rest when truncated. Binary files return a short marker rather than mojibake.",
745
821
  inputSchema: {
746
822
  type: "object",
747
823
  properties: {
748
824
  path: { type: "string", description: "Relative file path." },
749
825
  offset: { type: "integer", description: "1-indexed line number to start from. Default: 1." },
750
826
  limit: { type: "integer", description: "Max lines to return. Default: 2000. Set 0 for unlimited." },
751
- maxBytes: { type: "integer", description: "Hard byte cap regardless of line count. Default: 65536. Set 0 for unlimited." }
827
+ maxBytes: { type: "integer", description: "Hard byte cap on file content read, regardless of line count. Default: 65536. Set 0 for unlimited. The rendered output may be slightly larger than this cap when `lineNumbers` is on (each line carries a `<N>\\t` prefix)." },
828
+ lineNumbers: { type: "boolean", description: "Prefix each line with its 1-indexed line number. Default: true. Override the agent-wide `behavior.readLineNumbers` for this call." }
752
829
  },
753
830
  required: ["path"]
754
831
  }
755
832
  },
756
- async execute({ path, offset, limit, maxBytes }, ctx) {
833
+ async execute({ path, offset, limit, maxBytes, lineNumbers }, ctx) {
757
834
  const imgMedia = imageMediaTypeFor(path);
758
835
  if (imgMedia) {
759
836
  const sizeCap = maxBytes !== void 0 ? normalizeInteger(maxBytes, DEFAULT_IMAGE_BYTE_CAP) : DEFAULT_IMAGE_BYTE_CAP;
@@ -787,10 +864,11 @@ var readFile = {
787
864
  const offsetForKey = normalizeInteger(offset, 1);
788
865
  const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
789
866
  const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
867
+ const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
790
868
  const currentHash = readState ? hashContent(raw) : "";
791
869
  if (readState) {
792
870
  const prior = readState.get(absKey);
793
- if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey) {
871
+ if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers) {
794
872
  return `File ${path} unchanged since the previous read in this session \u2014 the prior result is still current.`;
795
873
  }
796
874
  }
@@ -805,10 +883,10 @@ var readFile = {
805
883
  const startIdx = Math.max(0, offsetN - 1);
806
884
  const endIdx = limitN > 0 ? Math.min(totalLines, startIdx + limitN) : totalLines;
807
885
  let slice = lines.slice(startIdx, endIdx);
808
- let bytesUsed = 0;
809
886
  let bytesCut = false;
810
887
  if (maxBytesN > 0) {
811
888
  const truncatedSlice = [];
889
+ let bytesUsed = 0;
812
890
  for (const line of slice) {
813
891
  const lineBytes = Buffer2.byteLength(line) + 1;
814
892
  if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
@@ -841,15 +919,16 @@ var readFile = {
841
919
  bytesCut = true;
842
920
  }
843
921
  }
844
- const body = slice.join("\n");
845
922
  const linesReturned = slice.length;
846
923
  const lastLineRead = startIdx + linesReturned;
924
+ const body = showLineNumbers ? slice.map((line, i) => `${startIdx + i + 1} ${line}`).join("\n") : slice.join("\n");
847
925
  if (readState) {
848
926
  readState.set(absKey, {
849
927
  contentHash: currentHash,
850
928
  offset: offsetN,
851
929
  limit: limitN,
852
930
  maxBytes: maxBytesN,
931
+ lineNumbers: showLineNumbers,
853
932
  mtimeMs: Date.now()
854
933
  });
855
934
  }
@@ -879,21 +958,6 @@ function normalizeInteger(value, fallback) {
879
958
  return fallback;
880
959
  return Math.floor(value);
881
960
  }
882
- var REPLACEMENT_RATIO_THRESHOLD = 0.01;
883
- var REPLACEMENT_MIN_COUNT = 5;
884
- function looksBinary(text) {
885
- const sample = text.length > BINARY_PROBE_BYTES ? text.slice(0, BINARY_PROBE_BYTES) : text;
886
- if (sample.includes("\0"))
887
- return true;
888
- if (sample.length === 0)
889
- return false;
890
- let replacementCount = 0;
891
- for (let i = 0; i < sample.length; i++) {
892
- if (sample.charCodeAt(i) === 65533)
893
- replacementCount++;
894
- }
895
- return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
896
- }
897
961
 
898
962
  // src/tools/shell.ts
899
963
  import { Buffer as Buffer3 } from "buffer";
@@ -932,37 +996,54 @@ var DEFAULT_MAX_OUTPUT_BYTES = 8192;
932
996
  var shell = {
933
997
  spec: {
934
998
  name: "shell",
935
- description: "Execute a shell command in the project root and return its combined stdout/stderr. Output is tail-priority truncated at 8 KB by default; errors and exit-code summaries live in the tail. Set maxOutputBytes=0 to disable truncation.",
999
+ description: "Execute a shell command in the project root and return its combined stdout/stderr. Output is tail-priority truncated at 8 KB by default; errors and exit-code summaries live in the tail. By default each call appends a `(exit N, Nms)` footer and surfaces non-empty stderr in a separate section even on success \u2014 set `metadata: false` to return only stdout. Set maxOutputBytes=0 to disable truncation.",
936
1000
  inputSchema: {
937
1001
  type: "object",
938
1002
  properties: {
939
1003
  command: { type: "string", description: "Shell command to run." },
940
1004
  timeout: { type: "integer", description: "Per-call timeout in milliseconds." },
941
- maxOutputBytes: { type: "integer", description: "Truncate combined stdout+stderr beyond this many bytes. Default: 8192. Set 0 for unlimited." }
1005
+ maxOutputBytes: { type: "integer", description: "Truncate combined stdout+stderr beyond this many bytes. Default: 8192. Set 0 for unlimited." },
1006
+ metadata: { type: "boolean", description: "Append `(exit N, Nms)` footer and surface non-empty stderr on success. Default: true." }
942
1007
  },
943
1008
  required: ["command"]
944
1009
  }
945
1010
  },
946
- async execute({ command, timeout, maxOutputBytes }, ctx) {
1011
+ async execute({ command, timeout, maxOutputBytes, metadata }, ctx) {
947
1012
  const execOpts = {};
948
1013
  if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0)
949
1014
  execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
950
1015
  const cmd = command;
1016
+ const wantMetadata = metadata !== false;
1017
+ const startedAt = Date.now();
951
1018
  const result = await ctx.execution.exec(ctx.handle, cmd, execOpts);
1019
+ const durationMs = Date.now() - startedAt;
952
1020
  const cap = normalizeCap(maxOutputBytes);
953
1021
  const semantic = interpretShellResult(cmd, result.exitCode);
954
- if (result.exitCode === 0)
955
- return truncateTail(result.stdout || "(no output)", cap);
1022
+ if (result.exitCode === 0) {
1023
+ const stdoutTail = truncateTail(result.stdout || "(no output)", cap);
1024
+ if (!wantMetadata)
1025
+ return stdoutTail;
1026
+ const stderrTrimmed = result.stderr.trim();
1027
+ const stderrSection = stderrTrimmed ? `
1028
+ [stderr]
1029
+ ${truncateTail(stderrTrimmed, Math.min(cap, 2048))}` : "";
1030
+ return `${stdoutTail}${stderrSection}
1031
+ (exit 0, ${durationMs}ms)`;
1032
+ }
956
1033
  if (!semantic.isError) {
957
1034
  const body = (result.stdout || result.stderr || "").trim();
958
1035
  const tail = truncateTail(body, cap);
959
- const footer = semantic.message ? `
1036
+ const semanticFooter = semantic.message ? `
960
1037
  (${semantic.message})` : "";
961
- return tail.length > 0 ? `${tail}${footer}` : semantic.message ?? "(no output)";
1038
+ const timingFooter = wantMetadata ? `
1039
+ (exit ${result.exitCode}, ${durationMs}ms)` : "";
1040
+ const head = tail.length > 0 ? tail : semantic.message ?? "(no output)";
1041
+ return `${head}${semanticFooter}${timingFooter}`;
962
1042
  }
963
1043
  const combined = `${result.stdout}
964
1044
  ${result.stderr}`.trim();
965
- return `Exit code ${result.exitCode}
1045
+ const header = wantMetadata ? `Exit code ${result.exitCode} (${durationMs}ms)` : `Exit code ${result.exitCode}`;
1046
+ return `${header}
966
1047
  ${truncateTail(combined, cap)}`;
967
1048
  }
968
1049
  };
@@ -996,15 +1077,6 @@ ${tail}`;
996
1077
  }
997
1078
 
998
1079
  // src/tools/skills-read.ts
999
- var SNIFF_BYTES = 8192;
1000
- function looksBinary2(text) {
1001
- const len = Math.min(text.length, SNIFF_BYTES);
1002
- for (let i = 0; i < len; i++) {
1003
- if (text.charCodeAt(i) === 0)
1004
- return true;
1005
- }
1006
- return false;
1007
- }
1008
1080
  function createSkillsReadTool(options) {
1009
1081
  const byName = new Map(options.catalog.map((s) => [s.name, s]));
1010
1082
  return {
@@ -1049,7 +1121,7 @@ function createSkillsReadTool(options) {
1049
1121
  const message = err instanceof Error ? err.message : String(err);
1050
1122
  return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
1051
1123
  }
1052
- if (looksBinary2(content)) {
1124
+ if (containsNullByte(content)) {
1053
1125
  return JSON.stringify({
1054
1126
  kind: "binary-unsupported",
1055
1127
  path: validated.absolutePath,
@@ -1062,12 +1134,8 @@ function createSkillsReadTool(options) {
1062
1134
  }
1063
1135
 
1064
1136
  // src/tools/skills-run-script.ts
1065
- var SINGLE_QUOTE_RE = /'/g;
1066
1137
  var ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
1067
1138
  var COLLAPSE_SLASHES_RE = /\/+/g;
1068
- function quoteShellArg(arg) {
1069
- return `'${arg.replace(SINGLE_QUOTE_RE, `'\\''`)}'`;
1070
- }
1071
1139
  function createSkillsRunScriptTool(options) {
1072
1140
  const byName = new Map(options.catalog.map((s) => [s.name, s]));
1073
1141
  const timeoutMs = options.scriptTimeoutMs ?? 6e4;
@@ -1114,7 +1182,7 @@ function createSkillsRunScriptTool(options) {
1114
1182
  const validated = validateResourcePath(joinedPath, skill.baseDir);
1115
1183
  if (!validated.valid)
1116
1184
  return `Error: ${validated.error}`;
1117
- const cmd = [validated.absolutePath, ...args].map(quoteShellArg).join(" ");
1185
+ const cmd = [validated.absolutePath, ...args].map(alwaysQuote).join(" ");
1118
1186
  try {
1119
1187
  const result = await ctx.execution.exec(ctx.handle, cmd, {
1120
1188
  timeout: Math.max(1, Math.round(timeoutMs / 1e3))
@@ -1556,6 +1624,74 @@ function applyTailCompaction(messages, threshold, keepTurns) {
1556
1624
  }
1557
1625
  return changed ? out : messages;
1558
1626
  }
1627
+ var STALE_READ_STUB = "[\u2026elided: file edited later in this run; re-read if still needed.]";
1628
+ function applyStaleReadElision(messages) {
1629
+ if (messages.length === 0)
1630
+ return messages;
1631
+ const resultByCallId = /* @__PURE__ */ new Map();
1632
+ for (const msg of messages) {
1633
+ for (const block of msg.content) {
1634
+ if (block.type === "tool_result" && typeof block.output === "string")
1635
+ resultByCallId.set(block.callId, block.output);
1636
+ }
1637
+ }
1638
+ const maxMutationIdxByPath = /* @__PURE__ */ new Map();
1639
+ const readCallInfo = /* @__PURE__ */ new Map();
1640
+ for (let i = 0; i < messages.length; i++) {
1641
+ for (const block of messages[i].content) {
1642
+ if (block.type !== "tool_call")
1643
+ continue;
1644
+ const path = block.input.path;
1645
+ if (typeof path !== "string")
1646
+ continue;
1647
+ if (block.name === "read_file") {
1648
+ readCallInfo.set(block.id, { path, msgIdx: i });
1649
+ continue;
1650
+ }
1651
+ const isEdit = block.name === "edit" || block.name === "multi_edit";
1652
+ const isWrite = block.name === "write_file";
1653
+ if (!isEdit && !isWrite)
1654
+ continue;
1655
+ const result = resultByCallId.get(block.id);
1656
+ if (typeof result !== "string")
1657
+ continue;
1658
+ const succeeded = isEdit ? result.startsWith("Edited ") : result.startsWith("Created ") || result.startsWith("Updated ");
1659
+ if (!succeeded)
1660
+ continue;
1661
+ const prior = maxMutationIdxByPath.get(path);
1662
+ if (prior === void 0 || i > prior)
1663
+ maxMutationIdxByPath.set(path, i);
1664
+ }
1665
+ }
1666
+ if (maxMutationIdxByPath.size === 0)
1667
+ return messages;
1668
+ const staleCallIds = /* @__PURE__ */ new Set();
1669
+ for (const [callId, info] of readCallInfo) {
1670
+ const lastMutationIdx = maxMutationIdxByPath.get(info.path);
1671
+ if (typeof lastMutationIdx === "number" && info.msgIdx < lastMutationIdx)
1672
+ staleCallIds.add(callId);
1673
+ }
1674
+ if (staleCallIds.size === 0)
1675
+ return messages;
1676
+ let changed = false;
1677
+ const out = messages.slice();
1678
+ for (let i = 0; i < out.length; i++) {
1679
+ const msg = out[i];
1680
+ let msgChanged = false;
1681
+ const newContent = msg.content.map((block) => {
1682
+ if (block.type !== "tool_result" || !staleCallIds.has(block.callId))
1683
+ return block;
1684
+ if (block.output === STALE_READ_STUB)
1685
+ return block;
1686
+ msgChanged = true;
1687
+ changed = true;
1688
+ return { ...block, output: STALE_READ_STUB };
1689
+ });
1690
+ if (msgChanged)
1691
+ out[i] = { ...msg, content: newContent };
1692
+ }
1693
+ return changed ? out : messages;
1694
+ }
1559
1695
  function sanitizeStoredToolResults(provider, messages) {
1560
1696
  if (provider.meta.capabilities?.vision !== false)
1561
1697
  return messages;
@@ -1663,7 +1799,9 @@ function wrapProviderError(err, ctx) {
1663
1799
  }
1664
1800
  async function executeTurn(ctx, turn) {
1665
1801
  const turnId = await ctx.generateTurnId();
1666
- const canonicalMessages = turnsToMessages(ctx.turns);
1802
+ let canonicalMessages = turnsToMessages(ctx.turns);
1803
+ if (ctx.elideStaleReads === true)
1804
+ canonicalMessages = applyStaleReadElision(canonicalMessages);
1667
1805
  const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
1668
1806
  let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
1669
1807
  if (ctx.compactStrategy === "tail") {
@@ -1718,11 +1856,12 @@ async function executeTurn(ctx, turn) {
1718
1856
  );
1719
1857
  } catch (err) {
1720
1858
  const errorUsage = { input: 0, output: 0 };
1859
+ const errorContent = currentText ? [{ type: "text", text: currentText }] : [{ type: "text", text: "[provider error before any output]" }];
1721
1860
  const errorTurn = {
1722
1861
  id: turnId,
1723
1862
  runId: ctx.runId,
1724
1863
  role: "assistant",
1725
- content: currentText ? [{ type: "text", text: currentText }] : [],
1864
+ content: errorContent,
1726
1865
  usage: errorUsage,
1727
1866
  createdAt: Date.now()
1728
1867
  };
@@ -2055,28 +2194,10 @@ async function executeToolsSequential(ctx, toolCalls, turnId) {
2055
2194
  if (ctx.signal.aborted)
2056
2195
  break;
2057
2196
  if (ctx.steeringQueue.length > 0) {
2058
- const steerMsg = ctx.steeringQueue.shift();
2059
- await ctx.hooks.callHook("steer:inject", { message: steerMsg });
2060
- for (const skipped of toolCalls.slice(toolCalls.indexOf(call))) {
2061
- results.push({ id: skipped.id, content: "Skipped: steering message received" });
2062
- }
2063
- const toolResultMsg = ctx.provider.toolResultsMessage(results);
2064
- ctx.turns.push({
2065
- id: await ctx.generateTurnId(),
2066
- runId: ctx.runId,
2067
- role: toolResultMsg.role,
2068
- content: toolResultMsg.content,
2069
- createdAt: Date.now()
2070
- });
2071
- const steerUserMsg = ctx.provider.userMessage(steerMsg);
2072
- ctx.turns.push({
2073
- id: await ctx.generateTurnId(),
2074
- runId: ctx.runId,
2075
- role: steerUserMsg.role,
2076
- content: steerUserMsg.content,
2077
- createdAt: Date.now()
2078
- });
2079
- return [];
2197
+ const fromIdx = toolCalls.indexOf(call);
2198
+ for (let i = fromIdx; i < toolCalls.length; i++)
2199
+ results.push({ id: toolCalls[i].id, content: "Skipped: steering message received" });
2200
+ return results;
2080
2201
  }
2081
2202
  const { result } = await executeSingleTool(ctx, call, turnId);
2082
2203
  results.push(result);
@@ -2097,29 +2218,25 @@ async function executeToolsParallel(ctx, toolCalls, turnId) {
2097
2218
  }
2098
2219
 
2099
2220
  // src/prompt.ts
2100
- function canonicalizePrompt(prompt, legacyImages) {
2221
+ function canonicalizePrompt(prompt) {
2101
2222
  if (prompt === void 0)
2102
2223
  return void 0;
2103
2224
  if (typeof prompt === "string") {
2104
- const hasImages = legacyImages && legacyImages.length > 0;
2105
- if (prompt.length === 0 && !hasImages)
2225
+ if (prompt.length === 0)
2106
2226
  return void 0;
2107
- const parts = [];
2108
- if (prompt.length > 0)
2109
- parts.push({ type: "text", text: prompt });
2110
- if (hasImages) {
2111
- for (const img of legacyImages) {
2112
- parts.push({
2113
- type: "image",
2114
- mediaType: img.source.media_type,
2115
- data: img.source.data
2116
- });
2117
- }
2118
- }
2119
- return parts;
2227
+ return [{ type: "text", text: prompt }];
2120
2228
  }
2121
2229
  if (prompt.length === 0)
2122
2230
  return void 0;
2231
+ for (const part of prompt) {
2232
+ if (!part || typeof part !== "object" || typeof part.type !== "string") {
2233
+ throw new Error("Invalid PromptPart: each part must be an object with a `type` field.");
2234
+ }
2235
+ const type = part.type;
2236
+ if (type !== "text" && type !== "image" && type !== "document") {
2237
+ throw new Error(`Invalid PromptPart type "${type}". Expected "text" | "image" | "document".`);
2238
+ }
2239
+ }
2123
2240
  const hasMeaningfulPart = prompt.some((part) => part.type === "text" && part.text.length > 0 || part.type === "image" || part.type === "document");
2124
2241
  if (!hasMeaningfulPart)
2125
2242
  return void 0;
@@ -2307,7 +2424,9 @@ function resolveBehavior(agentBehavior, runBehavior) {
2307
2424
  dedupReads: runBehavior?.dedupReads ?? agentBehavior?.dedupReads,
2308
2425
  dedupTools: runBehavior?.dedupTools ?? agentBehavior?.dedupTools,
2309
2426
  requireReadBeforeEdit: runBehavior?.requireReadBeforeEdit ?? agentBehavior?.requireReadBeforeEdit,
2310
- toolBudgets: runBehavior?.toolBudgets ?? agentBehavior?.toolBudgets
2427
+ toolBudgets: runBehavior?.toolBudgets ?? agentBehavior?.toolBudgets,
2428
+ readLineNumbers: runBehavior?.readLineNumbers ?? agentBehavior?.readLineNumbers,
2429
+ elideStaleReads: runBehavior?.elideStaleReads ?? agentBehavior?.elideStaleReads
2311
2430
  };
2312
2431
  }
2313
2432
  function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
@@ -2331,6 +2450,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2331
2450
  const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
2332
2451
  let resolvedSkills = null;
2333
2452
  let skillsCatalog = null;
2453
+ let skillsCleanup = () => {
2454
+ };
2334
2455
  const skillActivationState = createSkillActivationState({
2335
2456
  maxActive: skillsConfig?.maxActive
2336
2457
  });
@@ -2368,8 +2489,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2368
2489
  options.signal.addEventListener("abort", onExternalAbort, { once: true });
2369
2490
  }
2370
2491
  }
2371
- idlePromise = new Promise((resolve) => {
2372
- idleResolve = resolve;
2492
+ idlePromise = new Promise((resolve2) => {
2493
+ idleResolve = resolve2;
2373
2494
  });
2374
2495
  const childrenStats = [];
2375
2496
  const unregisterSpawnHook = hooks.hook("spawn:complete", (ctx) => {
@@ -2398,7 +2519,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2398
2519
  await warmup();
2399
2520
  }
2400
2521
  if (!skillsDisabled && skillsConfig && !resolvedSkills) {
2401
- resolvedSkills = await resolveSkills(skillsConfig);
2522
+ const bundle = await resolveSkills(skillsConfig);
2523
+ resolvedSkills = bundle.skills;
2524
+ skillsCleanup = bundle.cleanup;
2402
2525
  await hooks.callHook("skills:resolve", { skills: resolvedSkills });
2403
2526
  const skillsToolRegistered = skillsConfig?.tool !== false && resolvedSkills.length > 0;
2404
2527
  const catalogCtx = {
@@ -2431,7 +2554,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2431
2554
  const thinking = options.thinking ?? "off";
2432
2555
  const model = options.model ?? provider.meta.defaultModel;
2433
2556
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
2434
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets } = resolvedBehavior;
2557
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads } = resolvedBehavior;
2435
2558
  let system = options.system || agentSystem || "You are a helpful assistant.";
2436
2559
  if (skillsCatalog) {
2437
2560
  system = `${system}
@@ -2480,7 +2603,7 @@ ${skillsCatalog}`;
2480
2603
  if (options.system) {
2481
2604
  await hooks.callHook("system:before", { system: options.system });
2482
2605
  }
2483
- const promptParts = canonicalizePrompt(options.prompt, options.images);
2606
+ const promptParts = canonicalizePrompt(options.prompt);
2484
2607
  if (promptParts) {
2485
2608
  const promptMsg = buildPromptMessage(provider, promptParts);
2486
2609
  turns.push({
@@ -2583,6 +2706,7 @@ ${skillsCatalog}`;
2583
2706
  compactStrategy,
2584
2707
  compactThreshold,
2585
2708
  compactKeepTurns,
2709
+ ...elideStaleReads !== void 0 ? { elideStaleReads } : {},
2586
2710
  ...thinkingDecay !== void 0 ? { thinkingDecay } : {},
2587
2711
  runStartMs,
2588
2712
  runToolCounts: {}
@@ -2655,6 +2779,11 @@ ${skillsCatalog}`;
2655
2779
  return idlePromise ?? Promise.resolve();
2656
2780
  }
2657
2781
  async function reset() {
2782
+ if (running) {
2783
+ throw new Error(
2784
+ "Cannot reset() while the agent is running. Call `agent.abort()` and `await agent.waitForIdle()` first."
2785
+ );
2786
+ }
2658
2787
  conversationTurns = [];
2659
2788
  steeringQueue.length = 0;
2660
2789
  followUpQueue.length = 0;
@@ -2743,6 +2872,9 @@ ${skillsCatalog}`;
2743
2872
  await executionContext.destroy(executionHandle);
2744
2873
  executionHandle = null;
2745
2874
  }
2875
+ skillsCleanup();
2876
+ skillsCleanup = () => {
2877
+ };
2746
2878
  }
2747
2879
  if (eager && allMcpServers.length > 0) {
2748
2880
  void warmup().catch(() => {
@@ -2822,9 +2954,9 @@ async function raceWithTimeout(task, timeoutMs) {
2822
2954
  return task;
2823
2955
  let timer;
2824
2956
  try {
2825
- return await new Promise((resolve, reject) => {
2957
+ return await new Promise((resolve2, reject) => {
2826
2958
  timer = setTimeout(() => reject(new SpawnTimeoutError(timeoutMs)), timeoutMs);
2827
- task.then(resolve, reject);
2959
+ task.then(resolve2, reject);
2828
2960
  });
2829
2961
  } finally {
2830
2962
  if (timer)
@@ -3028,7 +3160,6 @@ function createSpawnTool(options = {}) {
3028
3160
  }
3029
3161
  };
3030
3162
  }
3031
- var spawn = createSpawnTool();
3032
3163
 
3033
3164
  // src/tools/write-file.ts
3034
3165
  import { Buffer as Buffer4 } from "buffer";
@@ -3076,6 +3207,5 @@ export {
3076
3207
  readFile,
3077
3208
  shell,
3078
3209
  createSpawnTool,
3079
- spawn,
3080
3210
  writeFile
3081
3211
  };