@cydm/pie 1.0.18 → 1.0.19

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.
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  Agent
4
- } from "./chunk-VE2HDCNB.js";
4
+ } from "./chunk-2R3XTLE2.js";
5
5
  import {
6
6
  FileSystemGateway,
7
7
  Type,
@@ -9,8 +9,9 @@ import {
9
9
  detectPlatform,
10
10
  getFileSystem,
11
11
  getPlatformConfig,
12
+ hashText,
12
13
  streamSimple
13
- } from "./chunk-5DA2D3K2.js";
14
+ } from "./chunk-Q2N6B5JN.js";
14
15
  import {
15
16
  __require
16
17
  } from "./chunk-TG2EQLX2.js";
@@ -1995,7 +1996,7 @@ function isTransientFileError(error) {
1995
1996
  return /^(EPERM|EACCES|EBUSY|ENOTEMPTY)$/.test(code) || /\b(EPERM|EACCES|EBUSY|locked|busy)\b/i.test(message);
1996
1997
  }
1997
1998
  function sleep(ms) {
1998
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1999
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1999
2000
  }
2000
2001
  async function retryFileOperation(operation, retryDelaysMs) {
2001
2002
  let lastError;
@@ -3038,15 +3039,15 @@ var AgentSessionAutoCompactScheduler = class {
3038
3039
  }
3039
3040
  ensurePendingWait() {
3040
3041
  if (this.pendingWait) return;
3041
- this.pendingWait = new Promise((resolve2) => {
3042
- this.resolvePendingWait = resolve2;
3042
+ this.pendingWait = new Promise((resolve3) => {
3043
+ this.resolvePendingWait = resolve3;
3043
3044
  });
3044
3045
  }
3045
3046
  resolvePending() {
3046
- const resolve2 = this.resolvePendingWait;
3047
+ const resolve3 = this.resolvePendingWait;
3047
3048
  this.pendingWait = void 0;
3048
3049
  this.resolvePendingWait = void 0;
3049
- resolve2?.();
3050
+ resolve3?.();
3050
3051
  }
3051
3052
  };
3052
3053
 
@@ -3124,29 +3125,29 @@ var AgentSessionAutoContinueController = class {
3124
3125
  this.options.emit({ type: "auto_continue_scheduled", reason });
3125
3126
  this.options.agent.followUp(message);
3126
3127
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
3127
- await new Promise((resolve2) => {
3128
+ await new Promise((resolve3) => {
3128
3129
  this.autoContinueTimer = setTimer(() => {
3129
3130
  this.autoContinueTimer = void 0;
3130
3131
  if (this.options.getDisposed()) {
3131
- resolve2();
3132
+ resolve3();
3132
3133
  return;
3133
3134
  }
3134
3135
  void this.options.continueTurn().catch(() => {
3135
- }).finally(resolve2);
3136
+ }).finally(resolve3);
3136
3137
  }, 0);
3137
3138
  });
3138
3139
  }
3139
3140
  ensurePending() {
3140
3141
  if (this.pendingAutoContinue) return;
3141
- this.pendingAutoContinue = new Promise((resolve2) => {
3142
- this.resolvePendingAutoContinue = resolve2;
3142
+ this.pendingAutoContinue = new Promise((resolve3) => {
3143
+ this.resolvePendingAutoContinue = resolve3;
3143
3144
  });
3144
3145
  }
3145
3146
  resolvePending() {
3146
- const resolve2 = this.resolvePendingAutoContinue;
3147
+ const resolve3 = this.resolvePendingAutoContinue;
3147
3148
  this.pendingAutoContinue = void 0;
3148
3149
  this.resolvePendingAutoContinue = void 0;
3149
- resolve2?.();
3150
+ resolve3?.();
3150
3151
  }
3151
3152
  };
3152
3153
 
@@ -3555,15 +3556,15 @@ var AgentSessionQueueDispatcher = class {
3555
3556
  }
3556
3557
  ensurePending() {
3557
3558
  if (this.pendingDispatch) return;
3558
- this.pendingDispatch = new Promise((resolve2) => {
3559
- this.resolvePendingDispatch = resolve2;
3559
+ this.pendingDispatch = new Promise((resolve3) => {
3560
+ this.resolvePendingDispatch = resolve3;
3560
3561
  });
3561
3562
  }
3562
3563
  resolvePending() {
3563
- const resolve2 = this.resolvePendingDispatch;
3564
+ const resolve3 = this.resolvePendingDispatch;
3564
3565
  this.pendingDispatch = void 0;
3565
3566
  this.resolvePendingDispatch = void 0;
3566
- resolve2?.();
3567
+ resolve3?.();
3567
3568
  }
3568
3569
  };
3569
3570
 
@@ -3717,15 +3718,15 @@ var AgentSessionRetryController = class {
3717
3718
  if (this.pendingRetry) {
3718
3719
  return;
3719
3720
  }
3720
- this.pendingRetry = new Promise((resolve2) => {
3721
- this.resolvePendingRetry = resolve2;
3721
+ this.pendingRetry = new Promise((resolve3) => {
3722
+ this.resolvePendingRetry = resolve3;
3722
3723
  });
3723
3724
  }
3724
3725
  resolvePending() {
3725
- const resolve2 = this.resolvePendingRetry;
3726
+ const resolve3 = this.resolvePendingRetry;
3726
3727
  this.pendingRetry = void 0;
3727
3728
  this.resolvePendingRetry = void 0;
3728
- resolve2?.();
3729
+ resolve3?.();
3729
3730
  }
3730
3731
  };
3731
3732
 
@@ -5443,9 +5444,98 @@ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
5443
5444
  return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
5444
5445
  }
5445
5446
 
5447
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/file-observation-state.ts
5448
+ function snapshotsEqual(left, right) {
5449
+ return left.mtimeMs === right.mtimeMs && left.size === right.size && left.contentHash === right.contentHash;
5450
+ }
5451
+ function snapshotsHaveSameContent(left, right) {
5452
+ return left.size === right.size && left.contentHash === right.contentHash;
5453
+ }
5454
+ function snapshotFromStatAndContent(stat, content) {
5455
+ return {
5456
+ mtimeMs: stat.mtime.getTime(),
5457
+ size: stat.size,
5458
+ contentHash: hashText(content)
5459
+ };
5460
+ }
5461
+ function createFileObservationState(options = {}) {
5462
+ const files = /* @__PURE__ */ new Map();
5463
+ const maxFiles = Math.max(1, Math.floor(options.maxFiles ?? 100));
5464
+ const maxRangesPerFile = Math.max(0, Math.floor(options.maxRangesPerFile ?? 8));
5465
+ function touchFile(absolutePath, observed) {
5466
+ files.delete(absolutePath);
5467
+ files.set(absolutePath, observed);
5468
+ while (files.size > maxFiles) {
5469
+ const oldest = files.keys().next().value;
5470
+ if (oldest === void 0) break;
5471
+ files.delete(oldest);
5472
+ }
5473
+ }
5474
+ function touchRange(readRanges, rangeKey, range) {
5475
+ if (maxRangesPerFile === 0) {
5476
+ readRanges.clear();
5477
+ return;
5478
+ }
5479
+ readRanges.delete(rangeKey);
5480
+ readRanges.set(rangeKey, range);
5481
+ while (readRanges.size > maxRangesPerFile) {
5482
+ const oldest = readRanges.keys().next().value;
5483
+ if (oldest === void 0) break;
5484
+ readRanges.delete(oldest);
5485
+ }
5486
+ }
5487
+ return {
5488
+ isUnchangedReadRange(absolutePath, rangeKey, snapshot, selectedContentHash) {
5489
+ const observed = files.get(absolutePath);
5490
+ const range = observed?.readRanges.get(rangeKey);
5491
+ if (!observed || !range) {
5492
+ return false;
5493
+ }
5494
+ const unchanged = snapshotsEqual(range.snapshot, snapshot) && range.selectedContentHash === selectedContentHash;
5495
+ touchRange(observed.readRanges, rangeKey, range);
5496
+ touchFile(absolutePath, observed);
5497
+ return unchanged;
5498
+ },
5499
+ recordTextRead(absolutePath, rangeKey, snapshot, selectedContentHash, completeTextKnown) {
5500
+ const observed = files.get(absolutePath) ?? {
5501
+ snapshot,
5502
+ completeTextKnown: false,
5503
+ readRanges: /* @__PURE__ */ new Map()
5504
+ };
5505
+ const preservesFreshCompleteRead = observed.completeTextKnown && snapshotsHaveSameContent(observed.snapshot, snapshot);
5506
+ observed.snapshot = snapshot;
5507
+ observed.completeTextKnown = completeTextKnown || preservesFreshCompleteRead;
5508
+ touchRange(observed.readRanges, rangeKey, { snapshot, selectedContentHash });
5509
+ touchFile(absolutePath, observed);
5510
+ },
5511
+ getCompleteTextSnapshot(absolutePath) {
5512
+ const observed = files.get(absolutePath);
5513
+ if (!observed?.completeTextKnown) {
5514
+ return void 0;
5515
+ }
5516
+ touchFile(absolutePath, observed);
5517
+ return observed.snapshot;
5518
+ },
5519
+ recordCompleteTextWrite(absolutePath, snapshot) {
5520
+ touchFile(absolutePath, {
5521
+ snapshot,
5522
+ completeTextKnown: true,
5523
+ readRanges: /* @__PURE__ */ new Map()
5524
+ });
5525
+ },
5526
+ invalidate(absolutePath) {
5527
+ files.delete(absolutePath);
5528
+ },
5529
+ clear() {
5530
+ files.clear();
5531
+ }
5532
+ };
5533
+ }
5534
+
5446
5535
  // ../../packages/shared-headless-capabilities/src/builtin/fs/read.ts
5447
5536
  function createReadTool(cwd, options) {
5448
5537
  const fs2 = getFileSystem();
5538
+ const fileObservationState = options?.fileObservationState ?? createFileObservationState();
5449
5539
  const rootSchema = buildRootParameterSchema(cwd, options);
5450
5540
  const readSchema = Type.Object({
5451
5541
  path: Type.String({ description: "Path to the file to read. Example: 'Scripts/PlayerController.cs' or 'Assets/Gen/config.json'." }),
@@ -5464,7 +5554,7 @@ function createReadTool(cwd, options) {
5464
5554
  const { path: path3, root, offset, limit, question } = params;
5465
5555
  const absolutePath = resolveReadPath(path3, cwd, options, root);
5466
5556
  return new Promise(
5467
- (resolve2, reject) => {
5557
+ (resolve3, reject) => {
5468
5558
  if (signal?.aborted) {
5469
5559
  reject(new Error("Operation aborted"));
5470
5560
  return;
@@ -5480,11 +5570,18 @@ function createReadTool(cwd, options) {
5480
5570
  (async () => {
5481
5571
  try {
5482
5572
  await fs2.access(absolutePath);
5573
+ if (isSpecialDevicePath(absolutePath)) {
5574
+ throw new Error(`Refusing to read special device path: ${absolutePath}`);
5575
+ }
5576
+ const stat = await fs2.stat(absolutePath);
5577
+ if (!stat.isFile) {
5578
+ throw new Error(`Refusing to read non-regular file: ${absolutePath}`);
5579
+ }
5483
5580
  if (aborted) return;
5484
5581
  const imageInfo = await readImageInfo(absolutePath, fs2);
5485
5582
  if (imageInfo) {
5486
5583
  if (question && options?.understandFile) {
5487
- resolve2(await options.understandFile({
5584
+ resolve3(await options.understandFile({
5488
5585
  absolutePath,
5489
5586
  displayPath: path3,
5490
5587
  mimeType: imageInfo.mimeType,
@@ -5496,7 +5593,7 @@ function createReadTool(cwd, options) {
5496
5593
  return;
5497
5594
  }
5498
5595
  const dimensionText = imageInfo.width && imageInfo.height ? `, ${imageInfo.width}x${imageInfo.height}` : "";
5499
- resolve2({
5596
+ resolve3({
5500
5597
  content: [
5501
5598
  { type: "text", text: `Read image file [${imageInfo.mimeType}${dimensionText}]. Original image attached as an image block; for very large images, prefer a smaller crop or resized copy before rereading.` },
5502
5599
  { type: "image", data: imageInfo.base64, mimeType: imageInfo.mimeType }
@@ -5513,7 +5610,7 @@ function createReadTool(cwd, options) {
5513
5610
  }
5514
5611
  const mediaInfo = detectUnderstandingKind(absolutePath);
5515
5612
  if (mediaInfo && options?.understandFile) {
5516
- resolve2(await options.understandFile({
5613
+ resolve3(await options.understandFile({
5517
5614
  absolutePath,
5518
5615
  displayPath: path3,
5519
5616
  mimeType: mediaInfo.mimeType,
@@ -5524,7 +5621,7 @@ function createReadTool(cwd, options) {
5524
5621
  return;
5525
5622
  }
5526
5623
  if (mediaInfo) {
5527
- resolve2({
5624
+ resolve3({
5528
5625
  content: [{
5529
5626
  type: "text",
5530
5627
  text: `Cannot read ${mediaInfo.kind} file ${path3} semantically: no media understanding handler is configured for ${mediaInfo.mimeType}.`
@@ -5540,24 +5637,62 @@ function createReadTool(cwd, options) {
5540
5637
  });
5541
5638
  return;
5542
5639
  }
5543
- const content = String(await fs2.readFile(absolutePath, "utf-8"));
5544
- const allLines = content.split("\n");
5545
- const totalFileLines = allLines.length;
5546
5640
  const startLine = offset ? Math.max(0, offset - 1) : 0;
5547
5641
  const startLineDisplay = startLine + 1;
5548
- if (startLine >= allLines.length) {
5549
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
5642
+ const range = await fs2.readTextRange(absolutePath, {
5643
+ offsetLine: startLine,
5644
+ limitLines: limit,
5645
+ maxOutputLines: DEFAULT_MAX_LINES,
5646
+ maxOutputBytes: DEFAULT_MAX_BYTES
5647
+ }, signal);
5648
+ const snapshot = {
5649
+ mtimeMs: range.mtimeMs,
5650
+ size: range.size,
5651
+ contentHash: range.contentHash
5652
+ };
5653
+ const totalFileLines = range.totalLines;
5654
+ if (startLine >= range.totalLines) {
5655
+ throw new Error(`Offset ${offset} is beyond end of file (${range.totalLines} lines total)`);
5550
5656
  }
5551
- let selectedContent;
5552
5657
  let userLimitedLines;
5553
5658
  if (limit !== void 0) {
5554
- const endLine = Math.min(startLine + limit, allLines.length);
5555
- selectedContent = allLines.slice(startLine, endLine).join("\n");
5556
- userLimitedLines = endLine - startLine;
5557
- } else {
5558
- selectedContent = allLines.slice(startLine).join("\n");
5659
+ userLimitedLines = range.selectedLines;
5660
+ }
5661
+ const rangeKey = `${startLineDisplay}:${limit === void 0 ? "all" : limit}`;
5662
+ const truncation = {
5663
+ content: range.content,
5664
+ truncated: range.truncated,
5665
+ truncatedBy: range.truncatedBy,
5666
+ totalLines: range.selectedLines,
5667
+ totalBytes: range.selectedBytes,
5668
+ outputLines: range.outputLines,
5669
+ outputBytes: range.outputBytes,
5670
+ lastLinePartial: false,
5671
+ firstLineExceedsLimit: range.firstLineExceedsLimit,
5672
+ maxLines: DEFAULT_MAX_LINES,
5673
+ maxBytes: DEFAULT_MAX_BYTES
5674
+ };
5675
+ const readStartsAtBeginning = offset === void 0 || offset === 1;
5676
+ const readReachedEndOfFile = startLine + range.selectedLines >= range.totalLines;
5677
+ const isCompleteTextKnown = readStartsAtBeginning && readReachedEndOfFile && !truncation.truncated && !truncation.firstLineExceedsLimit;
5678
+ if (fileObservationState.isUnchangedReadRange(absolutePath, rangeKey, snapshot, range.selectedContentHash)) {
5679
+ if (signal) {
5680
+ signal.removeEventListener("abort", onAbort);
5681
+ }
5682
+ resolve3({
5683
+ content: [{
5684
+ type: "text",
5685
+ text: "File unchanged since previous read of this range. Use a different offset/limit if you need another section."
5686
+ }],
5687
+ details: {
5688
+ unchangedSincePreviousRead: true,
5689
+ path: absolutePath,
5690
+ offset: startLineDisplay,
5691
+ limit
5692
+ }
5693
+ });
5694
+ return;
5559
5695
  }
5560
- const truncation = truncateHead(selectedContent);
5561
5696
  let outputText;
5562
5697
  let details;
5563
5698
  const sourceHeader = `File: ${absolutePath}
@@ -5565,7 +5700,7 @@ Base directory: ${nodePath.dirname(absolutePath)}
5565
5700
 
5566
5701
  `;
5567
5702
  if (truncation.firstLineExceedsLimit) {
5568
- const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
5703
+ const firstLineSize = formatSize(range.firstLineBytes);
5569
5704
  outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit.]`;
5570
5705
  details = { truncation };
5571
5706
  } else if (truncation.truncated) {
@@ -5582,8 +5717,8 @@ Base directory: ${nodePath.dirname(absolutePath)}
5582
5717
  [Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
5583
5718
  }
5584
5719
  details = { truncation };
5585
- } else if (userLimitedLines !== void 0 && startLine + userLimitedLines < allLines.length) {
5586
- const remaining = allLines.length - (startLine + userLimitedLines);
5720
+ } else if (userLimitedLines !== void 0 && startLine + userLimitedLines < range.totalLines) {
5721
+ const remaining = range.totalLines - (startLine + userLimitedLines);
5587
5722
  const nextOffset = startLine + userLimitedLines + 1;
5588
5723
  outputText = truncation.content;
5589
5724
  outputText += `
@@ -5592,13 +5727,20 @@ Base directory: ${nodePath.dirname(absolutePath)}
5592
5727
  } else {
5593
5728
  outputText = truncation.content;
5594
5729
  }
5730
+ fileObservationState.recordTextRead(
5731
+ absolutePath,
5732
+ rangeKey,
5733
+ snapshot,
5734
+ range.selectedContentHash,
5735
+ isCompleteTextKnown
5736
+ );
5595
5737
  outputText = sourceHeader + outputText;
5596
5738
  const textContent = [{ type: "text", text: outputText }];
5597
5739
  if (aborted) return;
5598
5740
  if (signal) {
5599
5741
  signal.removeEventListener("abort", onAbort);
5600
5742
  }
5601
- resolve2({ content: textContent, details });
5743
+ resolve3({ content: textContent, details });
5602
5744
  } catch (error) {
5603
5745
  if (signal) {
5604
5746
  signal.removeEventListener("abort", onAbort);
@@ -5613,6 +5755,10 @@ Base directory: ${nodePath.dirname(absolutePath)}
5613
5755
  }
5614
5756
  };
5615
5757
  }
5758
+ function isSpecialDevicePath(path3) {
5759
+ const normalized = nodePath.resolve(path3).replace(/\\/g, "/");
5760
+ return /^\/dev\/(?:zero|random|urandom|full|stdin|stdout|stderr|tty|console)$/.test(normalized) || /^\/dev\/fd\/[0-2]$/.test(normalized) || /^\/proc\/(?:self|\d+)\/fd\/[0-2]$/.test(normalized);
5761
+ }
5616
5762
  function detectUnderstandingKind(filePath) {
5617
5763
  const lower = filePath.toLowerCase();
5618
5764
  if (/\.(mp4|mov|m4v|webm|avi|mkv)$/.test(lower)) {
@@ -5734,6 +5880,8 @@ function parseWebpDimensions(bytes) {
5734
5880
  // ../../packages/shared-headless-capabilities/src/builtin/fs/write.ts
5735
5881
  function createWriteTool(cwd, options) {
5736
5882
  const fs2 = getFileSystem();
5883
+ const usesExternalFileObservationState = !!options?.fileObservationState;
5884
+ const fileObservationState = options?.fileObservationState ?? createFileObservationState();
5737
5885
  const rootSchema = buildRootParameterSchema(cwd, options);
5738
5886
  const writeSchema = Type.Object({
5739
5887
  path: Type.String({ description: "Path to the file to write." }),
@@ -5744,14 +5892,14 @@ function createWriteTool(cwd, options) {
5744
5892
  return {
5745
5893
  name: "write",
5746
5894
  label: "write",
5747
- description: `Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories. ${pathResolutionDescription}`,
5895
+ description: `Write complete file contents. Use this to create a new file, or to intentionally replace an entire existing file after reading the latest full file with read_file. Do not use for targeted edits to existing files; use edit_file instead. For existing files, omitted content will be deleted. Automatically creates parent directories. ${pathResolutionDescription}`,
5748
5896
  parameters: writeSchema,
5749
5897
  execute: async (_toolCallId, params, signal) => {
5750
5898
  const { path: filePath, root, content } = params;
5751
5899
  const absolutePath = resolveToCwd(filePath, cwd, options, root);
5752
5900
  const dir = fs2.dirname(absolutePath);
5753
5901
  return new Promise(
5754
- (resolve2, reject) => {
5902
+ (resolve3, reject) => {
5755
5903
  if (signal?.aborted) {
5756
5904
  reject(new Error("Operation aborted"));
5757
5905
  return;
@@ -5768,12 +5916,38 @@ function createWriteTool(cwd, options) {
5768
5916
  try {
5769
5917
  await fs2.mkdir(dir, { recursive: true });
5770
5918
  if (aborted) return;
5919
+ let fileExists = false;
5920
+ try {
5921
+ const stat = await fs2.stat(absolutePath);
5922
+ fileExists = stat.isFile;
5923
+ } catch {
5924
+ fileExists = false;
5925
+ }
5926
+ if (fileExists) {
5927
+ const currentContent = String(await fs2.readFile(absolutePath, "utf-8"));
5928
+ const currentStat = await fs2.stat(absolutePath);
5929
+ const currentSnapshot = snapshotFromStatAndContent(currentStat, currentContent);
5930
+ const observedSnapshot = fileObservationState.getCompleteTextSnapshot(absolutePath);
5931
+ if (!observedSnapshot) {
5932
+ throw new Error(
5933
+ usesExternalFileObservationState ? "Read the current full file with read_file before overwriting it, or use edit_file for targeted changes." : "Existing-file overwrite requires write_file to share a fileObservationState with read_file; use createSharedFileSystemTools or pass the same fileObservationState to both tools."
5934
+ );
5935
+ }
5936
+ if (!snapshotsHaveSameContent(observedSnapshot, currentSnapshot)) {
5937
+ throw new Error("File changed since it was read. Read the latest file contents before overwriting it.");
5938
+ }
5939
+ }
5771
5940
  await fs2.writeFile(absolutePath, content, "utf-8");
5941
+ const writtenStat = await fs2.stat(absolutePath);
5942
+ fileObservationState.recordCompleteTextWrite(
5943
+ absolutePath,
5944
+ snapshotFromStatAndContent(writtenStat, content)
5945
+ );
5772
5946
  if (aborted) return;
5773
5947
  if (signal) {
5774
5948
  signal.removeEventListener("abort", onAbort);
5775
5949
  }
5776
- resolve2({
5950
+ resolve3({
5777
5951
  content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` }],
5778
5952
  details: void 0
5779
5953
  });
@@ -5839,28 +6013,30 @@ function buildFuzzyIndexMap(text) {
5839
6013
  }
5840
6014
  return { normalized, originalStartByNormalizedIndex, originalEndByNormalizedIndex };
5841
6015
  }
5842
- function countOccurrences(content, needle) {
5843
- if (needle.length === 0) return 0;
5844
- let count = 0;
6016
+ function collectMatches(content, needle) {
6017
+ if (needle.length === 0) return [];
6018
+ const matches = [];
5845
6019
  let index = 0;
5846
6020
  while (true) {
5847
6021
  const nextIndex = content.indexOf(needle, index);
5848
6022
  if (nextIndex === -1) break;
5849
- count++;
6023
+ matches.push({ index: nextIndex, matchLength: needle.length });
5850
6024
  index = nextIndex + needle.length;
5851
6025
  }
5852
- return count;
6026
+ return matches;
5853
6027
  }
5854
6028
  function fuzzyFindText(content, oldText) {
5855
6029
  const exactIndex = content.indexOf(oldText);
5856
6030
  if (exactIndex !== -1) {
6031
+ const matches2 = collectMatches(content, oldText);
5857
6032
  return {
5858
6033
  found: true,
5859
6034
  index: exactIndex,
5860
6035
  matchLength: oldText.length,
5861
6036
  usedFuzzyMatch: false,
5862
6037
  contentForReplacement: content,
5863
- occurrences: countOccurrences(content, oldText),
6038
+ occurrences: matches2.length,
6039
+ matches: matches2,
5864
6040
  matchKind: "exact"
5865
6041
  };
5866
6042
  }
@@ -5875,6 +6051,7 @@ function fuzzyFindText(content, oldText) {
5875
6051
  usedFuzzyMatch: false,
5876
6052
  contentForReplacement: content,
5877
6053
  occurrences: 0,
6054
+ matches: [],
5878
6055
  matchKind: "none"
5879
6056
  };
5880
6057
  }
@@ -5887,19 +6064,26 @@ function fuzzyFindText(content, oldText) {
5887
6064
  usedFuzzyMatch: false,
5888
6065
  contentForReplacement: content,
5889
6066
  occurrences: 0,
6067
+ matches: [],
5890
6068
  matchKind: "none"
5891
6069
  };
5892
6070
  }
5893
- const endFuzzyIndex = fuzzyIndex + fuzzyOldText.length - 1;
5894
- const originalStart = fuzzyMap.originalStartByNormalizedIndex[fuzzyIndex];
5895
- const originalEnd = fuzzyMap.originalEndByNormalizedIndex[endFuzzyIndex];
6071
+ const fuzzyMatches = collectMatches(fuzzyContent, fuzzyOldText);
6072
+ const matches = fuzzyMatches.map((match) => {
6073
+ const endFuzzyIndex = match.index + match.matchLength - 1;
6074
+ const originalStart = fuzzyMap.originalStartByNormalizedIndex[match.index] ?? match.index;
6075
+ const originalEnd = fuzzyMap.originalEndByNormalizedIndex[endFuzzyIndex] ?? originalStart + match.matchLength;
6076
+ return { index: originalStart, matchLength: originalEnd - originalStart };
6077
+ });
6078
+ const firstMatch = matches[0] ?? { index: -1, matchLength: 0 };
5896
6079
  return {
5897
6080
  found: true,
5898
- index: originalStart,
5899
- matchLength: originalEnd - originalStart,
6081
+ index: firstMatch.index,
6082
+ matchLength: firstMatch.matchLength,
5900
6083
  usedFuzzyMatch: true,
5901
6084
  contentForReplacement: content,
5902
- occurrences: countOccurrences(fuzzyContent, fuzzyOldText),
6085
+ occurrences: matches.length,
6086
+ matches,
5903
6087
  matchKind: "fuzzy"
5904
6088
  };
5905
6089
  }
@@ -5947,7 +6131,7 @@ function generateDiffString(oldContent, newContent, contextLines = 4) {
5947
6131
  }
5948
6132
 
5949
6133
  // ../../packages/shared-headless-capabilities/src/builtin/fs/edit.ts
5950
- function validateEditArgs(path3, oldText, newText) {
6134
+ function validateEditArgs(path3, oldText, newText, replaceAll) {
5951
6135
  const filePath = typeof path3 === "string" ? path3.trim() : "";
5952
6136
  if (!filePath) {
5953
6137
  throw new Error("Invalid edit_file arguments: path cannot be empty. Pass the file path in path and the exact text to replace in oldText.");
@@ -5961,11 +6145,14 @@ function validateEditArgs(path3, oldText, newText) {
5961
6145
  if (oldText === newText) {
5962
6146
  throw new Error("Invalid edit_file arguments: oldText and newText are identical, so the edit would make no change.");
5963
6147
  }
6148
+ if (replaceAll !== void 0 && typeof replaceAll !== "boolean") {
6149
+ throw new Error("Invalid edit_file arguments: replaceAll must be a boolean when provided.");
6150
+ }
5964
6151
  if (filePath.includes("<|tool") || oldText.includes("<|tool") || newText.includes("<|tool")) {
5965
6152
  throw new Error("Invalid edit_file arguments: path, oldText, and newText must be real edit values, not tool-call markup.");
5966
6153
  }
5967
6154
  }
5968
- function countOccurrences2(content, needle) {
6155
+ function countOccurrences(content, needle) {
5969
6156
  if (needle.length === 0) return 0;
5970
6157
  let count = 0;
5971
6158
  let index = 0;
@@ -5992,6 +6179,8 @@ function createEditErrorResult(params) {
5992
6179
  diff: "",
5993
6180
  oldTextOccurrences: params.oldTextOccurrences,
5994
6181
  newTextOccurrences: params.newTextOccurrences,
6182
+ replaceAll: params.replaceAll,
6183
+ replacedOccurrences: 0,
5995
6184
  errorReason: params.errorReason,
5996
6185
  recoveryHint: params.recoveryHint,
5997
6186
  staleBaseLikely: params.staleBaseLikely,
@@ -6002,12 +6191,14 @@ function createEditErrorResult(params) {
6002
6191
  }
6003
6192
  function createEditTool(cwd, options) {
6004
6193
  const fs2 = getFileSystem();
6194
+ const fileObservationState = options?.fileObservationState ?? createFileObservationState();
6005
6195
  const rootSchema = buildRootParameterSchema(cwd, options);
6006
6196
  const editSchema = Type.Object({
6007
6197
  path: Type.String({ description: "Path to the file to edit." }),
6008
6198
  ...rootSchema ? { root: rootSchema } : {},
6009
6199
  oldText: Type.String({ description: "Exact text to find and replace (must match exactly)" }),
6010
- newText: Type.String({ description: "New text to replace the old text with" })
6200
+ newText: Type.String({ description: "New text to replace the old text with" }),
6201
+ replaceAll: Type.Optional(Type.Boolean({ description: "Replace every occurrence of oldText. Only set this to true when every matching occurrence should be replaced." }))
6011
6202
  });
6012
6203
  const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
6013
6204
  return {
@@ -6016,10 +6207,11 @@ function createEditTool(cwd, options) {
6016
6207
  description: `Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits. ${pathResolutionDescription}`,
6017
6208
  parameters: editSchema,
6018
6209
  execute: async (_toolCallId, params, signal) => {
6019
- const { path: path3, root, oldText, newText } = params;
6020
- validateEditArgs(path3, oldText, newText);
6210
+ const { path: path3, root, oldText, newText, replaceAll } = params;
6211
+ validateEditArgs(path3, oldText, newText, replaceAll);
6212
+ const shouldReplaceAll = replaceAll === true;
6021
6213
  const absolutePath = resolveToCwd(path3, cwd, options, root);
6022
- return new Promise((resolve2, reject) => {
6214
+ return new Promise((resolve3, reject) => {
6023
6215
  if (signal?.aborted) {
6024
6216
  reject(new Error("Operation aborted"));
6025
6217
  return;
@@ -6049,50 +6241,57 @@ function createEditTool(cwd, options) {
6049
6241
  const normalizedContent = normalizeToLF(content);
6050
6242
  const normalizedOldText = normalizeToLF(oldText);
6051
6243
  const normalizedNewText = normalizeToLF(newText);
6052
- const newTextOccurrences = countOccurrences2(normalizedContent, normalizedNewText);
6244
+ const newTextOccurrences = countOccurrences(normalizedContent, normalizedNewText);
6053
6245
  const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
6054
6246
  if (!matchResult.found) {
6055
6247
  if (signal) signal.removeEventListener("abort", onAbort);
6056
- resolve2(
6248
+ resolve3(
6057
6249
  createEditErrorResult({
6058
6250
  message: `Could not find oldText in ${path3}.`,
6059
6251
  matchKind: "none",
6060
6252
  errorReason: "old_text_not_found",
6061
6253
  oldTextOccurrences: 0,
6062
6254
  newTextOccurrences,
6255
+ replaceAll: shouldReplaceAll,
6063
6256
  staleBaseLikely: newTextOccurrences > 0,
6064
6257
  alreadyAppliedCandidate: newTextOccurrences > 0,
6065
- recoveryHint: newTextOccurrences > 0 ? "The replacement text already appears in the current file. Read the current file before retrying; this may be a stale oldText from an earlier edit in the same turn." : "Read the current file and retry with a larger exact block copied from the latest file contents."
6258
+ recoveryHint: newTextOccurrences > 0 ? "The replacement text already appears in the current file. Read the current file before retrying; this may be a stale oldText from an earlier edit in the same turn." : "Read the current file and retry with exact text copied from the latest file contents."
6066
6259
  })
6067
6260
  );
6068
6261
  return;
6069
6262
  }
6070
- if (matchResult.occurrences > 1) {
6263
+ if (matchResult.occurrences > 1 && !shouldReplaceAll) {
6071
6264
  if (signal) signal.removeEventListener("abort", onAbort);
6072
- resolve2(
6265
+ resolve3(
6073
6266
  createEditErrorResult({
6074
6267
  message: `Found ${matchResult.occurrences} occurrences of oldText in ${path3}. The text must be unique.`,
6075
6268
  matchKind: matchResult.matchKind,
6076
6269
  errorReason: "old_text_not_unique",
6077
6270
  oldTextOccurrences: matchResult.occurrences,
6078
6271
  newTextOccurrences,
6079
- recoveryHint: "Read the current file and retry with a larger exact block that uniquely identifies the intended location."
6272
+ replaceAll: shouldReplaceAll,
6273
+ recoveryHint: "Read the current file and retry with a larger exact block copied from the latest file contents, or set replaceAll to true if every occurrence should be replaced."
6080
6274
  })
6081
6275
  );
6082
6276
  return;
6083
6277
  }
6084
6278
  if (aborted) return;
6085
- const baseContent = normalizedContent;
6086
- const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
6279
+ const baseContent = matchResult.contentForReplacement;
6280
+ const matchesToReplace = shouldReplaceAll ? matchResult.matches : matchResult.matches.slice(0, 1);
6281
+ let newContent = baseContent;
6282
+ for (const match of [...matchesToReplace].sort((left, right) => right.index - left.index)) {
6283
+ newContent = newContent.substring(0, match.index) + normalizedNewText + newContent.substring(match.index + match.matchLength);
6284
+ }
6087
6285
  if (baseContent === newContent) {
6088
6286
  if (signal) signal.removeEventListener("abort", onAbort);
6089
- resolve2(
6287
+ resolve3(
6090
6288
  createEditErrorResult({
6091
6289
  message: `No changes made to ${path3}. The replacement produced identical content.`,
6092
6290
  matchKind: matchResult.matchKind,
6093
6291
  errorReason: "replacement_no_change",
6094
6292
  oldTextOccurrences: matchResult.occurrences,
6095
6293
  newTextOccurrences,
6294
+ replaceAll: shouldReplaceAll,
6096
6295
  recoveryHint: "Read the current file and verify whether the intended edit is already present or whether oldText/newText need a larger exact block.",
6097
6296
  alreadyAppliedCandidate: newTextOccurrences > 0
6098
6297
  })
@@ -6101,16 +6300,17 @@ function createEditTool(cwd, options) {
6101
6300
  }
6102
6301
  const finalContent = bom + restoreLineEndings(newContent, originalEnding);
6103
6302
  await fs2.writeFile(absolutePath, finalContent, "utf-8");
6303
+ fileObservationState.invalidate(absolutePath);
6104
6304
  if (aborted) return;
6105
6305
  if (signal) {
6106
6306
  signal.removeEventListener("abort", onAbort);
6107
6307
  }
6108
6308
  const diffResult = generateDiffString(content, restoreLineEndings(newContent, originalEnding));
6109
- resolve2({
6309
+ resolve3({
6110
6310
  content: [
6111
6311
  {
6112
6312
  type: "text",
6113
- text: `Successfully replaced text in ${path3} using ${matchResult.matchKind} match.`
6313
+ text: `Successfully replaced text in ${path3} using ${matchResult.matchKind} match (${matchesToReplace.length} occurrence${matchesToReplace.length === 1 ? "" : "s"}).`
6114
6314
  },
6115
6315
  { type: "text", text: diffResult.diff }
6116
6316
  ],
@@ -6121,7 +6321,9 @@ function createEditTool(cwd, options) {
6121
6321
  diff: diffResult.diff,
6122
6322
  firstChangedLine: diffResult.firstChangedLine,
6123
6323
  oldTextOccurrences: matchResult.occurrences,
6124
- newTextOccurrences
6324
+ newTextOccurrences,
6325
+ replaceAll: shouldReplaceAll,
6326
+ replacedOccurrences: matchesToReplace.length
6125
6327
  }
6126
6328
  });
6127
6329
  } catch (error) {
@@ -6155,7 +6357,7 @@ function createLsTool(cwd, options) {
6155
6357
  description: `List directory contents. Use this to inspect folders before reading or writing files. Example: list_dir path='Scripts' or list_dir path='Data'. ${pathResolutionDescription} Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
6156
6358
  parameters: lsSchema,
6157
6359
  execute: async (_toolCallId, params, signal) => {
6158
- return new Promise((resolve2, reject) => {
6360
+ return new Promise((resolve3, reject) => {
6159
6361
  if (signal?.aborted) {
6160
6362
  reject(new Error("Operation aborted"));
6161
6363
  return;
@@ -6205,7 +6407,7 @@ function createLsTool(cwd, options) {
6205
6407
  }
6206
6408
  signal?.removeEventListener("abort", onAbort);
6207
6409
  if (results.length === 0) {
6208
- resolve2({ content: [{ type: "text", text: "(empty directory)" }], details: void 0 });
6410
+ resolve3({ content: [{ type: "text", text: "(empty directory)" }], details: void 0 });
6209
6411
  return;
6210
6412
  }
6211
6413
  const rawOutput = results.join("\n");
@@ -6226,7 +6428,7 @@ function createLsTool(cwd, options) {
6226
6428
 
6227
6429
  [${notices.join(". ")}]`;
6228
6430
  }
6229
- resolve2({
6431
+ resolve3({
6230
6432
  content: [{ type: "text", text: output }],
6231
6433
  details: Object.keys(details).length > 0 ? details : void 0
6232
6434
  });
@@ -6402,11 +6604,11 @@ function waitForFileCompletion(requestId) {
6402
6604
  if (existing) {
6403
6605
  return Promise.resolve(existing);
6404
6606
  }
6405
- return new Promise((resolve2) => {
6406
- store.waiters.set(requestId, resolve2);
6607
+ return new Promise((resolve3) => {
6608
+ store.waiters.set(requestId, resolve3);
6407
6609
  });
6408
6610
  }
6409
- async function runNativeGrep(searchPath, pattern, globPattern, ignoreCase, literal, contextLines, limit, signal) {
6611
+ async function runNativeGrep(searchPath, pattern, globPattern, ignoreCase, literal, contextLines, limit, outputMode, offset, signal) {
6410
6612
  const bridge = getNativeFileBridge();
6411
6613
  if (!bridge) {
6412
6614
  throw new Error("CS.Pie.PieFileBridge not available");
@@ -6418,7 +6620,9 @@ async function runNativeGrep(searchPath, pattern, globPattern, ignoreCase, liter
6418
6620
  !!ignoreCase,
6419
6621
  !!literal,
6420
6622
  contextLines,
6421
- limit
6623
+ limit,
6624
+ outputMode,
6625
+ offset
6422
6626
  );
6423
6627
  const onAbort = () => bridge.CancelRequest(requestId);
6424
6628
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -6443,6 +6647,45 @@ async function runNativeGrep(searchPath, pattern, globPattern, ignoreCase, liter
6443
6647
  bridge.ReleaseRequest(requestId);
6444
6648
  }
6445
6649
  }
6650
+ function normalizeOutputMode(value) {
6651
+ if (value === void 0 || value === null || value === "content") return "content";
6652
+ if (value === "files_with_matches" || value === "count") return value;
6653
+ throw new Error("Invalid grep_text arguments: outputMode must be one of content, files_with_matches, or count.");
6654
+ }
6655
+ function normalizeNonNegativeInteger(value, fallback, name) {
6656
+ if (value === void 0 || value === null) return fallback;
6657
+ const numberValue = Number(value);
6658
+ if (!Number.isInteger(numberValue) || numberValue < 0) {
6659
+ throw new Error(`Invalid grep_text arguments: ${name} must be a non-negative integer.`);
6660
+ }
6661
+ return numberValue;
6662
+ }
6663
+ function formatContentMatches(matches, contextLines) {
6664
+ const outputLines = [];
6665
+ let linesTruncated = false;
6666
+ for (const match of matches) {
6667
+ const start = contextLines > 0 ? Math.max(0, match.lineIndex - contextLines) : match.lineIndex;
6668
+ const end = contextLines > 0 ? Math.min(match.lines.length - 1, match.lineIndex + contextLines) : match.lineIndex;
6669
+ for (let c = start; c <= end; c++) {
6670
+ const lineText = match.lines[c];
6671
+ const { text: truncatedText, wasTruncated } = truncateLine(lineText);
6672
+ if (wasTruncated) linesTruncated = true;
6673
+ const currentLineNum = c + 1;
6674
+ if (c === match.lineIndex) {
6675
+ outputLines.push(`${match.relativePath}:${currentLineNum}: ${truncatedText}`);
6676
+ } else {
6677
+ outputLines.push(`${match.relativePath}-${currentLineNum}- ${truncatedText}`);
6678
+ }
6679
+ }
6680
+ }
6681
+ return { lines: outputLines, linesTruncated };
6682
+ }
6683
+ function sortFileSummariesByMtime(files) {
6684
+ return files.sort((left, right) => {
6685
+ if (right.mtimeMs !== left.mtimeMs) return right.mtimeMs - left.mtimeMs;
6686
+ return left.relativePath.localeCompare(right.relativePath);
6687
+ });
6688
+ }
6446
6689
  async function collectFiles(dirPath, globPattern, fs2, isGitignored = () => false) {
6447
6690
  const files = [];
6448
6691
  const queue = [{ dir: dirPath, relPrefix: "" }];
@@ -6502,13 +6745,19 @@ function createGrepTextTool(cwd, options) {
6502
6745
  context: Type.Optional(
6503
6746
  Type.Number({ description: "Number of lines to show before and after each match (default: 0)" })
6504
6747
  ),
6748
+ outputMode: Type.Optional(Type.Union([
6749
+ Type.Literal("content"),
6750
+ Type.Literal("files_with_matches"),
6751
+ Type.Literal("count")
6752
+ ], { description: "Result mode: content returns matching lines, files_with_matches returns matching file paths, count returns per-file match counts." })),
6753
+ offset: Type.Optional(Type.Number({ description: "Number of results to skip before returning output (default: 0)." })),
6505
6754
  limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" }))
6506
6755
  });
6507
6756
  const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
6508
6757
  return {
6509
6758
  name: "grep_text",
6510
6759
  label: "grep_text",
6511
- description: `Search file contents for a pattern. ${pathResolutionDescription} Returns matching lines with file paths and line numbers. Output is truncated to ${DEFAULT_LIMIT2} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
6760
+ description: `Search file contents for a pattern. ${pathResolutionDescription} outputMode='content' returns matching lines with file paths and line numbers, outputMode='files_with_matches' returns matching file paths, and outputMode='count' returns per-file match counts. Output is truncated to ${DEFAULT_LIMIT2} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
6512
6761
  parameters: grepSchema,
6513
6762
  execute: async (_toolCallId, params, signal) => {
6514
6763
  const {
@@ -6519,9 +6768,11 @@ function createGrepTextTool(cwd, options) {
6519
6768
  ignoreCase,
6520
6769
  literal,
6521
6770
  context: contextLines,
6771
+ outputMode: rawOutputMode,
6772
+ offset: rawOffset,
6522
6773
  limit
6523
6774
  } = params;
6524
- return new Promise((resolve2, reject) => {
6775
+ return new Promise((resolve3, reject) => {
6525
6776
  if (signal?.aborted) {
6526
6777
  reject(new Error("Operation aborted"));
6527
6778
  return;
@@ -6531,6 +6782,8 @@ function createGrepTextTool(cwd, options) {
6531
6782
  (async () => {
6532
6783
  try {
6533
6784
  const searchPath = resolveToCwd(searchDir || ".", cwd, options, root);
6785
+ const outputMode = normalizeOutputMode(rawOutputMode);
6786
+ const offset = normalizeNonNegativeInteger(rawOffset, 0, "offset");
6534
6787
  const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT2);
6535
6788
  const ctx = contextLines && contextLines > 0 ? contextLines : 0;
6536
6789
  let isDir;
@@ -6558,6 +6811,8 @@ function createGrepTextTool(cwd, options) {
6558
6811
  let matchCount = 0;
6559
6812
  let matchLimitReached = false;
6560
6813
  let linesTruncated = false;
6814
+ let filesScanned = 0;
6815
+ let totalFilesWithMatches = 0;
6561
6816
  const isGitignored = await loadGitignoreMatcher(searchPath, fs2);
6562
6817
  if (platform === "puerts" && getNativeFileBridge()) {
6563
6818
  const nativeResult = await runNativeGrep(
@@ -6568,12 +6823,16 @@ function createGrepTextTool(cwd, options) {
6568
6823
  !!literal,
6569
6824
  ctx,
6570
6825
  effectiveLimit,
6826
+ outputMode,
6827
+ offset,
6571
6828
  signal
6572
6829
  );
6573
6830
  outputLines = nativeResult.Lines ?? [];
6574
- matchCount = nativeResult.MatchCount ?? 0;
6831
+ matchCount = nativeResult.TotalMatches ?? nativeResult.MatchCount ?? 0;
6575
6832
  matchLimitReached = !!nativeResult.MatchLimitReached;
6576
6833
  linesTruncated = !!nativeResult.LinesTruncated;
6834
+ filesScanned = nativeResult.FilesScanned ?? 0;
6835
+ totalFilesWithMatches = nativeResult.TotalFilesWithMatches ?? 0;
6577
6836
  globalThis.pieBridge?.log?.(
6578
6837
  "info",
6579
6838
  `[grep_text] path=${searchPath} pattern=${pattern} matches=${matchCount} files=${nativeResult.FilesScanned ?? 0}`
@@ -6586,11 +6845,9 @@ function createGrepTextTool(cwd, options) {
6586
6845
  } else {
6587
6846
  filePaths = [searchPath];
6588
6847
  }
6848
+ const contentMatches = [];
6849
+ const fileSummaries = /* @__PURE__ */ new Map();
6589
6850
  for (const filePath of filePaths) {
6590
- if (matchCount >= effectiveLimit) {
6591
- matchLimitReached = true;
6592
- break;
6593
- }
6594
6851
  if (signal?.aborted) {
6595
6852
  reject(new Error("Operation aborted"));
6596
6853
  return;
@@ -6603,36 +6860,65 @@ function createGrepTextTool(cwd, options) {
6603
6860
  }
6604
6861
  const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
6605
6862
  const relativePath = isDir ? fs2.relative(searchPath, filePath) : fs2.basename(filePath);
6863
+ filesScanned++;
6864
+ let fileMatchCount = 0;
6606
6865
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
6607
- if (matchCount >= effectiveLimit) {
6608
- matchLimitReached = true;
6609
- break;
6610
- }
6611
6866
  regex.lastIndex = 0;
6612
6867
  if (regex.test(lines[lineIdx])) {
6613
6868
  matchCount++;
6614
- const start = ctx > 0 ? Math.max(0, lineIdx - ctx) : lineIdx;
6615
- const end = ctx > 0 ? Math.min(lines.length - 1, lineIdx + ctx) : lineIdx;
6616
- for (let c = start; c <= end; c++) {
6617
- const lineText = lines[c];
6618
- const { text: truncatedText, wasTruncated } = truncateLine(lineText);
6619
- if (wasTruncated) linesTruncated = true;
6620
- const currentLineNum = c + 1;
6621
- if (c === lineIdx) {
6622
- outputLines.push(`${relativePath}:${currentLineNum}: ${truncatedText}`);
6623
- } else {
6624
- outputLines.push(`${relativePath}-${currentLineNum}- ${truncatedText}`);
6625
- }
6869
+ fileMatchCount++;
6870
+ if (outputMode === "content" && matchCount > offset && matchCount <= offset + effectiveLimit) {
6871
+ contentMatches.push({ filePath, relativePath, lineIndex: lineIdx, lines });
6626
6872
  }
6627
6873
  }
6628
6874
  }
6875
+ if (fileMatchCount > 0) {
6876
+ const fileStat = await fs2.stat(filePath);
6877
+ fileSummaries.set(filePath, {
6878
+ filePath,
6879
+ relativePath,
6880
+ mtimeMs: fileStat.mtime.getTime(),
6881
+ matchCount: fileMatchCount
6882
+ });
6883
+ }
6884
+ }
6885
+ totalFilesWithMatches = fileSummaries.size;
6886
+ if (outputMode === "content") {
6887
+ matchLimitReached = matchCount > offset + effectiveLimit;
6888
+ const formatted = formatContentMatches(contentMatches.slice(0, effectiveLimit), ctx);
6889
+ outputLines = formatted.lines;
6890
+ linesTruncated = formatted.linesTruncated;
6891
+ } else {
6892
+ const sortedFiles = sortFileSummariesByMtime([...fileSummaries.values()]);
6893
+ const page = sortedFiles.slice(offset, offset + effectiveLimit);
6894
+ if (offset + effectiveLimit < sortedFiles.length) {
6895
+ matchLimitReached = true;
6896
+ }
6897
+ if (outputMode === "files_with_matches") {
6898
+ outputLines = page.map((file) => file.relativePath);
6899
+ } else {
6900
+ outputLines = [
6901
+ `Total matches: ${matchCount}`,
6902
+ `Files with matches: ${sortedFiles.length}`,
6903
+ ...page.map((file) => `${file.relativePath}: ${file.matchCount}`)
6904
+ ];
6905
+ }
6629
6906
  }
6630
6907
  }
6631
6908
  signal?.removeEventListener("abort", onAbort);
6632
6909
  if (matchCount === 0) {
6633
- resolve2({
6910
+ resolve3({
6634
6911
  content: [{ type: "text", text: "No matches found" }],
6635
- details: { noMatches: true }
6912
+ details: {
6913
+ noMatches: true,
6914
+ outputMode,
6915
+ totalMatches: 0,
6916
+ totalFilesWithMatches: 0,
6917
+ filesScanned,
6918
+ offset,
6919
+ limit: effectiveLimit,
6920
+ resultLimitReached: false
6921
+ }
6636
6922
  });
6637
6923
  return;
6638
6924
  }
@@ -6640,10 +6926,17 @@ function createGrepTextTool(cwd, options) {
6640
6926
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
6641
6927
  let output = truncation.content;
6642
6928
  const details = {};
6929
+ details.outputMode = outputMode;
6930
+ details.totalMatches = matchCount;
6931
+ details.totalFilesWithMatches = totalFilesWithMatches;
6932
+ details.filesScanned = filesScanned;
6933
+ details.offset = offset;
6934
+ details.limit = effectiveLimit;
6935
+ details.resultLimitReached = matchLimitReached;
6643
6936
  const notices = [];
6644
6937
  if (matchLimitReached) {
6645
6938
  notices.push(
6646
- `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
6939
+ outputMode === "content" ? `${effectiveLimit} matches limit reached. Use offset=${offset + effectiveLimit} for more, increase limit, or refine pattern` : `${effectiveLimit} results limit reached. Use offset=${offset + effectiveLimit} for more, increase limit, or refine pattern`
6647
6940
  );
6648
6941
  details.matchLimitReached = effectiveLimit;
6649
6942
  }
@@ -6662,7 +6955,7 @@ function createGrepTextTool(cwd, options) {
6662
6955
 
6663
6956
  [${notices.join(". ")}]`;
6664
6957
  }
6665
- resolve2({
6958
+ resolve3({
6666
6959
  content: [{ type: "text", text: output }],
6667
6960
  details: Object.keys(details).length > 0 ? details : void 0
6668
6961
  });
@@ -6745,8 +7038,8 @@ function waitForFileCompletion2(requestId) {
6745
7038
  if (existing) {
6746
7039
  return Promise.resolve(existing);
6747
7040
  }
6748
- return new Promise((resolve2) => {
6749
- store.waiters.set(requestId, resolve2);
7041
+ return new Promise((resolve3) => {
7042
+ store.waiters.set(requestId, resolve3);
6750
7043
  });
6751
7044
  }
6752
7045
  async function runNativeFind(searchPath, pattern, limit, signal) {
@@ -6836,7 +7129,7 @@ function createFindFilesTool(cwd, options) {
6836
7129
  parameters: findSchema,
6837
7130
  execute: async (_toolCallId, params, signal) => {
6838
7131
  const { pattern, path: searchDir, root, limit } = params;
6839
- return new Promise((resolve2, reject) => {
7132
+ return new Promise((resolve3, reject) => {
6840
7133
  if (signal?.aborted) {
6841
7134
  reject(new Error("Operation aborted"));
6842
7135
  return;
@@ -6852,10 +7145,12 @@ function createFindFilesTool(cwd, options) {
6852
7145
  return;
6853
7146
  }
6854
7147
  let results;
7148
+ let resultLimitReached = false;
6855
7149
  const isGitignored = await loadGitignoreMatcher(searchPath, fs2);
6856
7150
  if (platform === "puerts" && getNativeFileBridge2()) {
6857
7151
  const nativeResult = await runNativeFind(searchPath, pattern, effectiveLimit, signal);
6858
7152
  results = (nativeResult.Results ?? []).filter((filePath) => !isGitignored(filePath, false));
7153
+ resultLimitReached = !!nativeResult.LimitReached && results.length >= effectiveLimit;
6859
7154
  globalThis.pieBridge?.log?.(
6860
7155
  "info",
6861
7156
  `[find_files] path=${searchPath} pattern=${pattern} matches=${results.length} dirs=${nativeResult.ScannedDirectories ?? 0} files=${nativeResult.ScannedFiles ?? 0}`
@@ -6864,28 +7159,33 @@ function createFindFilesTool(cwd, options) {
6864
7159
  try {
6865
7160
  const _require = globalThis.require || __require;
6866
7161
  const { globSync } = _require("glob");
6867
- results = globSync(pattern, {
7162
+ const allResults = globSync(pattern, {
6868
7163
  cwd: searchPath,
6869
7164
  ignore: ["**/node_modules/**", "**/.git/**"],
6870
7165
  nodir: false,
6871
7166
  absolute: false,
6872
7167
  dot: true
6873
- }).filter((filePath) => !isGitignored(filePath, false)).slice(0, effectiveLimit);
7168
+ }).filter((filePath) => !isGitignored(filePath, false));
7169
+ resultLimitReached = allResults.length > effectiveLimit;
7170
+ results = allResults.slice(0, effectiveLimit);
6874
7171
  } catch {
6875
- results = await globFiles(searchPath, pattern, fs2, effectiveLimit, signal, isGitignored);
7172
+ const allResults = await globFiles(searchPath, pattern, fs2, effectiveLimit + 1, signal, isGitignored);
7173
+ resultLimitReached = allResults.length > effectiveLimit;
7174
+ results = allResults.slice(0, effectiveLimit);
6876
7175
  }
6877
7176
  } else {
6878
- results = await globFiles(searchPath, pattern, fs2, effectiveLimit, signal, isGitignored);
7177
+ const allResults = await globFiles(searchPath, pattern, fs2, effectiveLimit + 1, signal, isGitignored);
7178
+ resultLimitReached = allResults.length > effectiveLimit;
7179
+ results = allResults.slice(0, effectiveLimit);
6879
7180
  }
6880
7181
  signal?.removeEventListener("abort", onAbort);
6881
7182
  if (results.length === 0) {
6882
- resolve2({
7183
+ resolve3({
6883
7184
  content: [{ type: "text", text: "No files found matching pattern" }],
6884
7185
  details: { noMatches: true }
6885
7186
  });
6886
7187
  return;
6887
7188
  }
6888
- const resultLimitReached = results.length >= effectiveLimit;
6889
7189
  const rawOutput = results.join("\n");
6890
7190
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
6891
7191
  let resultOutput = truncation.content;
@@ -6902,7 +7202,7 @@ function createFindFilesTool(cwd, options) {
6902
7202
  if (notices.length > 0) {
6903
7203
  resultOutput += "\n\n[" + notices.join(". ") + "]";
6904
7204
  }
6905
- resolve2({
7205
+ resolve3({
6906
7206
  content: [{ type: "text", text: resultOutput }],
6907
7207
  details: Object.keys(details).length > 0 ? details : void 0
6908
7208
  });
@@ -6918,10 +7218,12 @@ function createFindFilesTool(cwd, options) {
6918
7218
 
6919
7219
  // ../../packages/shared-headless-capabilities/src/builtin/fs/index.ts
6920
7220
  function createSharedFileSystemTools(sandboxRoot, options) {
7221
+ const fileObservationState = options?.fileObservationState ?? createFileObservationState();
7222
+ const sharedOptions = { ...options, fileObservationState };
6921
7223
  return [
6922
- createReadTool(sandboxRoot, options),
6923
- createWriteTool(sandboxRoot, options),
6924
- createEditTool(sandboxRoot, options),
7224
+ createReadTool(sandboxRoot, sharedOptions),
7225
+ createWriteTool(sandboxRoot, sharedOptions),
7226
+ createEditTool(sandboxRoot, sharedOptions),
6925
7227
  createLsTool(sandboxRoot, options),
6926
7228
  createGrepTextTool(sandboxRoot, options),
6927
7229
  createFindFilesTool(sandboxRoot, options)
@@ -8526,8 +8828,8 @@ async function runQueuedSearch(model, fn) {
8526
8828
  const key = `${model.provider}/${model.id}`;
8527
8829
  const previous = SEARCH_PROVIDER_QUEUE.get(key) ?? Promise.resolve();
8528
8830
  let release;
8529
- const next = new Promise((resolve2) => {
8530
- release = resolve2;
8831
+ const next = new Promise((resolve3) => {
8832
+ release = resolve3;
8531
8833
  });
8532
8834
  const queued = previous.catch(() => void 0).then(() => next);
8533
8835
  SEARCH_PROVIDER_QUEUE.set(key, queued);
@@ -8898,10 +9200,10 @@ function assistantText(message) {
8898
9200
  async function withAbort(promise, signal) {
8899
9201
  if (!signal) return promise;
8900
9202
  if (signal.aborted) throw signal.reason ?? new Error("operation aborted");
8901
- return await new Promise((resolve2, reject) => {
9203
+ return await new Promise((resolve3, reject) => {
8902
9204
  const onAbort = () => reject(signal.reason ?? new Error("operation aborted"));
8903
9205
  signal.addEventListener("abort", onAbort, { once: true });
8904
- promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", onAbort));
9206
+ promise.then(resolve3, reject).finally(() => signal.removeEventListener("abort", onAbort));
8905
9207
  });
8906
9208
  }
8907
9209
  async function applyPromptWithModel(deps, prompt, url, content, signal) {
@@ -9402,13 +9704,13 @@ async function runWithDeadline(operation, timeoutMs, message, parentSignal) {
9402
9704
  const child = createChildSignal(parentSignal);
9403
9705
  let timer;
9404
9706
  try {
9405
- return await new Promise((resolve2, reject) => {
9707
+ return await new Promise((resolve3, reject) => {
9406
9708
  timer = setTimeout(() => {
9407
9709
  const error = new Error(message);
9408
9710
  child.abort(error);
9409
9711
  reject(error);
9410
9712
  }, timeoutMs);
9411
- operation(child.signal).then(resolve2, reject);
9713
+ operation(child.signal).then(resolve3, reject);
9412
9714
  });
9413
9715
  } finally {
9414
9716
  if (timer) clearTimeout(timer);
@@ -9972,6 +10274,7 @@ export {
9972
10274
  buildProjectContextSection,
9973
10275
  createAskUserCapability,
9974
10276
  createPolicyEnforcedTools,
10277
+ createFileObservationState,
9975
10278
  createSharedFileSystemTools,
9976
10279
  buildToolsPromptSection,
9977
10280
  createSharedWebSearchTool,