indusagi 0.12.34 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/agent.js +1281 -184
  3. package/dist/ai.js +106 -4
  4. package/dist/capabilities.js +69 -2
  5. package/dist/cli.js +83 -13
  6. package/dist/connectors-saas.js +66 -0
  7. package/dist/index.js +83 -13
  8. package/dist/interop.js +66 -0
  9. package/dist/mcp.js +270 -363
  10. package/dist/react-ink.js +15 -11
  11. package/dist/shell-app.js +83 -13
  12. package/dist/smithy.js +69 -2
  13. package/dist/swarm.js +69 -2
  14. package/dist/types/capabilities/backends/node-backends.d.ts +3 -1
  15. package/dist/types/capabilities/files/read-state-gate.d.ts +69 -0
  16. package/dist/types/capabilities/files/read-state-gate.test.d.ts +14 -0
  17. package/dist/types/capabilities/kernel/context.d.ts +4 -0
  18. package/dist/types/capabilities/kernel/index.d.ts +2 -2
  19. package/dist/types/capabilities/kernel/spec.d.ts +55 -0
  20. package/dist/types/facade/bot/actions/bash.d.ts +15 -0
  21. package/dist/types/facade/bot/actions/bash.test.d.ts +1 -0
  22. package/dist/types/facade/bot/actions/checkpoint.d.ts +49 -0
  23. package/dist/types/facade/bot/actions/checkpoint.test.d.ts +1 -0
  24. package/dist/types/facade/bot/actions/edit-utils.d.ts +86 -0
  25. package/dist/types/facade/bot/actions/edit.d.ts +18 -0
  26. package/dist/types/facade/bot/actions/edit.test.d.ts +1 -0
  27. package/dist/types/facade/bot/actions/find.d.ts +2 -0
  28. package/dist/types/facade/bot/actions/find.test.d.ts +1 -0
  29. package/dist/types/facade/bot/actions/grep.d.ts +10 -0
  30. package/dist/types/facade/bot/actions/grep.test.d.ts +1 -0
  31. package/dist/types/facade/bot/actions/index.d.ts +16 -0
  32. package/dist/types/facade/bot/actions/read-state.d.ts +83 -0
  33. package/dist/types/facade/bot/actions/read-state.test.d.ts +1 -0
  34. package/dist/types/facade/bot/actions/read.d.ts +7 -0
  35. package/dist/types/facade/bot/actions/read.test.d.ts +1 -0
  36. package/dist/types/facade/bot/actions/sandbox-backend.d.ts +99 -0
  37. package/dist/types/facade/bot/actions/sandbox-backend.test.d.ts +1 -0
  38. package/dist/types/facade/bot/actions/websearch.d.ts +5 -2
  39. package/dist/types/facade/bot/actions/websearch.test.d.ts +1 -0
  40. package/dist/types/facade/bot/actions/write.d.ts +15 -0
  41. package/dist/types/facade/bot/agent-loop.d.ts +10 -0
  42. package/dist/types/facade/bot/agent-loop.test.d.ts +1 -0
  43. package/dist/types/facade/bot/agent.d.ts +9 -1
  44. package/dist/types/facade/bot/permission-gate.test.d.ts +1 -0
  45. package/dist/types/facade/bot/types.d.ts +60 -0
  46. package/dist/types/facade/mcp-core/client.d.ts +71 -15
  47. package/dist/types/facade/mcp-core/client.test.d.ts +18 -0
  48. package/dist/types/facade/mcp-core/types.d.ts +10 -0
  49. package/dist/types/facade/ml/adapters/anthropic-retry.test.d.ts +1 -0
  50. package/dist/types/facade/ml/adapters/anthropic.d.ts +17 -0
  51. package/dist/types/facade/ml/adapters/simple-options.d.ts +13 -0
  52. package/dist/types/facade/ml/adapters/simple-options.test.d.ts +1 -0
  53. package/dist/types/facade/ml/models.generated.d.ts +34 -0
  54. package/dist/types/react-ink/components/StatusLine.d.ts +10 -1
  55. package/dist/types/react-ink/components/ToolEventBlock.d.ts +2 -1
  56. package/dist/types/react-ink/components/ToolEventBlock.test.d.ts +1 -0
  57. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -3509,6 +3509,23 @@ var MODELS = {
3509
3509
  },
3510
3510
  contextWindow: 204800,
3511
3511
  maxTokens: 131072
3512
+ },
3513
+ "MiniMax-M3": {
3514
+ id: "MiniMax-M3",
3515
+ name: "MiniMax-M3",
3516
+ api: "anthropic-messages",
3517
+ provider: "minimax",
3518
+ baseUrl: "https://api.minimax.io/anthropic",
3519
+ reasoning: true,
3520
+ input: ["text", "image"],
3521
+ cost: {
3522
+ input: 0.6,
3523
+ output: 2.4,
3524
+ cacheRead: 0.12,
3525
+ cacheWrite: 0
3526
+ },
3527
+ contextWindow: 1048576,
3528
+ maxTokens: 512e3
3512
3529
  }
3513
3530
  },
3514
3531
  "minimax-cn": {
@@ -3579,6 +3596,23 @@ var MODELS = {
3579
3596
  },
3580
3597
  contextWindow: 204800,
3581
3598
  maxTokens: 131072
3599
+ },
3600
+ "MiniMax-M3": {
3601
+ id: "MiniMax-M3",
3602
+ name: "MiniMax-M3",
3603
+ api: "anthropic-messages",
3604
+ provider: "minimax-cn",
3605
+ baseUrl: "https://api.minimaxi.com/anthropic",
3606
+ reasoning: true,
3607
+ input: ["text", "image"],
3608
+ cost: {
3609
+ input: 0.6,
3610
+ output: 2.4,
3611
+ cacheRead: 0.12,
3612
+ cacheWrite: 0
3613
+ },
3614
+ contextWindow: 1048576,
3615
+ maxTokens: 512e3
3582
3616
  }
3583
3617
  },
3584
3618
  "mistral": {
@@ -13006,24 +13040,58 @@ function normalizeProviderError(error) {
13006
13040
  }
13007
13041
  return new SimpleOptionsProviderError("Unknown provider error", "unknown", error);
13008
13042
  }
13043
+ function abortReason(signal) {
13044
+ const reason = signal.reason;
13045
+ if (reason instanceof SimpleOptionsProviderError) return reason;
13046
+ if (reason instanceof Error) return new SimpleOptionsProviderError(reason.message, "unknown", reason);
13047
+ return new SimpleOptionsProviderError("Request was aborted", "unknown", reason);
13048
+ }
13049
+ function abortableSleep(ms, signal) {
13050
+ return new Promise((resolve5, reject) => {
13051
+ if (signal?.aborted) {
13052
+ reject(abortReason(signal));
13053
+ return;
13054
+ }
13055
+ let onAbort;
13056
+ const timer = setTimeout(() => {
13057
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
13058
+ resolve5();
13059
+ }, ms);
13060
+ if (signal) {
13061
+ onAbort = () => {
13062
+ clearTimeout(timer);
13063
+ signal.removeEventListener("abort", onAbort);
13064
+ reject(abortReason(signal));
13065
+ };
13066
+ signal.addEventListener("abort", onAbort, { once: true });
13067
+ }
13068
+ });
13069
+ }
13009
13070
  async function executeWithRetry(operation, policy) {
13010
13071
  let attempt = 0;
13011
13072
  let lastError;
13012
13073
  while (attempt < policy.maxAttempts) {
13074
+ if (policy.signal?.aborted) {
13075
+ throw abortReason(policy.signal);
13076
+ }
13013
13077
  attempt++;
13014
13078
  try {
13015
13079
  return await operation();
13016
13080
  } catch (error) {
13017
13081
  lastError = error;
13018
13082
  const normalized = normalizeProviderError(error);
13083
+ if (policy.signal?.aborted) {
13084
+ throw normalized;
13085
+ }
13019
13086
  const defaultRetryable = normalized.code === "rate_limit" || normalized.code === "timeout" || normalized.code === "network";
13020
13087
  const retryable = policy.shouldRetry ? policy.shouldRetry(error, attempt) : defaultRetryable;
13021
13088
  if (!retryable || attempt >= policy.maxAttempts) {
13022
13089
  throw normalized;
13023
13090
  }
13024
13091
  const maxDelay = policy.maxDelayMs ?? Number.MAX_SAFE_INTEGER;
13025
- const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), maxDelay);
13026
- await new Promise((resolve5) => setTimeout(resolve5, backoff));
13092
+ const retryAfterMs = policy.getRetryAfterMs?.(error) ?? null;
13093
+ const backoff = retryAfterMs != null ? Math.min(retryAfterMs, maxDelay) : Math.min(policy.baseDelayMs * 2 ** (attempt - 1), maxDelay);
13094
+ await abortableSleep(backoff, policy.signal);
13027
13095
  }
13028
13096
  }
13029
13097
  throw normalizeProviderError(lastError);
@@ -13499,6 +13567,30 @@ var AnthropicEventReducer = class {
13499
13567
  calculateCost(this.model, this.output.usage);
13500
13568
  }
13501
13569
  };
13570
+ function parseAnthropicRetryAfterMs(error) {
13571
+ if (error instanceof Anthropic.APIError) {
13572
+ const header = error.headers?.get?.("retry-after");
13573
+ const seconds = header ? parseInt(header, 10) : Number.NaN;
13574
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
13575
+ }
13576
+ return null;
13577
+ }
13578
+ function shouldRetryAnthropic(error, _attempt) {
13579
+ if (error instanceof Anthropic.APIError) {
13580
+ const status = error.status;
13581
+ if (status === 408 || status === 409 || status === 429 || status === 529) return true;
13582
+ if (typeof status === "number" && status >= 500) return true;
13583
+ if (typeof status === "number" && status >= 400 && status < 500) return false;
13584
+ const body = `${error.message} ${JSON.stringify(error.error ?? "")}`.toLowerCase();
13585
+ if (body.includes('"type":"overloaded_error"') || body.includes("overloaded")) return true;
13586
+ return false;
13587
+ }
13588
+ if (error instanceof Error) {
13589
+ const msg = error.message.toLowerCase();
13590
+ return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("timeout") || msg.includes("timed out") || msg.includes("network") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("overloaded");
13591
+ }
13592
+ return false;
13593
+ }
13502
13594
  var streamAnthropic = (model, context, options) => {
13503
13595
  const stream = new AssistantMessageEventStream();
13504
13596
  const output = createAssistantMessageOutput(model);
@@ -13526,8 +13618,16 @@ var streamAnthropic = (model, context, options) => {
13526
13618
  });
13527
13619
  },
13528
13620
  {
13529
- maxAttempts: options?.signal ? 1 : 3,
13530
- baseDelayMs: 250
13621
+ // Transient retries are NO LONGER gated on signal presence:
13622
+ // the live path always carries a signal, so the old ternary
13623
+ // collapsed to a single attempt = zero retries on 429/529/5xx.
13624
+ // Abort still short-circuits immediately (see executeWithRetry).
13625
+ maxAttempts: 3,
13626
+ baseDelayMs: 500,
13627
+ maxDelayMs: 32e3,
13628
+ signal: options?.signal,
13629
+ shouldRetry: shouldRetryAnthropic,
13630
+ getRetryAfterMs: parseAnthropicRetryAfterMs
13531
13631
  }
13532
13632
  );
13533
13633
  if (options?.signal?.aborted) {
@@ -18407,6 +18507,63 @@ var AgentTelemetry = class {
18407
18507
  // src/facade/bot/agent-loop.ts
18408
18508
  var errorHandler = new AgentErrorHandler();
18409
18509
  var telemetry = new AgentTelemetry();
18510
+ var READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
18511
+ "read",
18512
+ "ls",
18513
+ "grep",
18514
+ "find",
18515
+ "websearch",
18516
+ "webfetch",
18517
+ "todoread"
18518
+ ]);
18519
+ function maxToolConcurrency(override) {
18520
+ if (typeof override === "number" && Number.isFinite(override) && override >= 1) {
18521
+ return Math.floor(override);
18522
+ }
18523
+ const fromEnv = parseInt(
18524
+ process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || process.env.INDUS_MAX_TOOL_CONCURRENCY || "",
18525
+ 10
18526
+ );
18527
+ return Number.isFinite(fromEnv) && fromEnv >= 1 ? fromEnv : 10;
18528
+ }
18529
+ function isConcurrencySafe(toolCall, tools, readOnly) {
18530
+ if (!readOnly.has(toolCall.name)) return false;
18531
+ return Boolean(tools?.some((candidate) => candidate.name === toolCall.name));
18532
+ }
18533
+ function partitionToolCalls(calls, tools, readOnly) {
18534
+ const batches = [];
18535
+ for (const call of calls) {
18536
+ const concurrent = isConcurrencySafe(call, tools, readOnly);
18537
+ const trailing = batches[batches.length - 1];
18538
+ if (concurrent && trailing && trailing.concurrent) {
18539
+ trailing.calls.push(call);
18540
+ } else {
18541
+ batches.push({ concurrent, calls: [call] });
18542
+ }
18543
+ }
18544
+ return batches;
18545
+ }
18546
+ async function runToolBatchConcurrently(calls, tools, signal, stream, limit, canUseTool) {
18547
+ const results = new Array(calls.length);
18548
+ let next = 0;
18549
+ const poolSize = Math.max(1, Math.min(limit, calls.length));
18550
+ const worker = async () => {
18551
+ for (; ; ) {
18552
+ const index = next++;
18553
+ if (index >= calls.length) return;
18554
+ const toolCall = calls[index];
18555
+ const key = `tool:${toolCall.name}:${toolCall.id}`;
18556
+ telemetry.start(key);
18557
+ results[index] = await executeToolCall(toolCall, tools, signal, stream, {
18558
+ emitMessageEvents: true,
18559
+ canUseTool
18560
+ });
18561
+ telemetry.log(`tool:${toolCall.name}`, telemetry.end(key));
18562
+ }
18563
+ };
18564
+ await Promise.all(Array.from({ length: poolSize }, () => worker()));
18565
+ return results;
18566
+ }
18410
18567
  function agentLoop(prompts, context, config, signal, streamFn) {
18411
18568
  const stream = createAgentStream();
18412
18569
  void (async () => {
@@ -18496,7 +18653,12 @@ async function runLoop(liveContext, produced, config, signal, stream, streamFn)
18496
18653
  assistantMessage,
18497
18654
  signal,
18498
18655
  stream,
18499
- config.getSteeringMessages
18656
+ config.getSteeringMessages,
18657
+ {
18658
+ readOnlyToolNames: config.readOnlyToolNames ?? READ_ONLY_TOOL_NAMES,
18659
+ maxConcurrency: maxToolConcurrency(config.maxToolConcurrency),
18660
+ canUseTool: config.canUseTool
18661
+ }
18500
18662
  );
18501
18663
  for (const result of dispatch.toolResults) {
18502
18664
  toolResults.push(result);
@@ -18581,7 +18743,40 @@ async function executeToolCall(toolCall, tools, signal, stream, options) {
18581
18743
  let isError = false;
18582
18744
  try {
18583
18745
  if (!matchedTool) throw new Error(`No registered tool named "${toolCall.name}".`);
18584
- const validatedArgs = validateToolArguments(matchedTool, toolCall);
18746
+ let validatedArgs = validateToolArguments(matchedTool, toolCall);
18747
+ if (options.canUseTool) {
18748
+ const decision = await options.canUseTool(toolCall.name, validatedArgs, { signal });
18749
+ if (decision.behavior === "deny") {
18750
+ const denied = {
18751
+ content: [{ type: "text", text: decision.message }],
18752
+ details: {},
18753
+ isError: true
18754
+ };
18755
+ stream.push({
18756
+ type: "tool_execution_end",
18757
+ toolCallId: toolCall.id,
18758
+ toolName: toolCall.name,
18759
+ result: denied,
18760
+ isError: true
18761
+ });
18762
+ const deniedMessage = {
18763
+ role: "toolResult",
18764
+ toolCallId: toolCall.id,
18765
+ toolName: toolCall.name,
18766
+ content: denied.content,
18767
+ details: denied.details,
18768
+ isError: true,
18769
+ timestamp: Date.now()
18770
+ };
18771
+ if (options.emitMessageEvents) {
18772
+ emitMessageStartEnd(stream, deniedMessage);
18773
+ }
18774
+ return deniedMessage;
18775
+ }
18776
+ if (decision.updatedInput !== void 0) {
18777
+ validatedArgs = decision.updatedInput;
18778
+ }
18779
+ }
18585
18780
  result = await matchedTool.execute(toolCall.id, validatedArgs, signal, (partialResult) => {
18586
18781
  stream.push({
18587
18782
  type: "tool_execution_update",
@@ -18621,16 +18816,30 @@ async function executeToolCall(toolCall, tools, signal, stream, options) {
18621
18816
  }
18622
18817
  return toolResultMessage;
18623
18818
  }
18624
- async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) {
18819
+ async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages, options) {
18625
18820
  const requestedCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
18821
+ const readOnly = options?.readOnlyToolNames ?? READ_ONLY_TOOL_NAMES;
18822
+ const limit = options?.maxConcurrency ?? maxToolConcurrency();
18823
+ const canUseTool = options?.canUseTool;
18626
18824
  const results = [];
18627
18825
  let steeringMessages;
18628
- for (let index = 0; index < requestedCalls.length; index++) {
18629
- const toolCall = requestedCalls[index];
18630
- telemetry.start(`tool:${toolCall.name}`);
18631
- const toolResult = await executeToolCall(toolCall, tools, signal, stream, { emitMessageEvents: true });
18632
- telemetry.log(`tool:${toolCall.name}`, telemetry.end(`tool:${toolCall.name}`));
18633
- results.push(toolResult);
18826
+ const batches = partitionToolCalls(requestedCalls, tools, readOnly);
18827
+ let dispatched = 0;
18828
+ for (const batch of batches) {
18829
+ if (batch.concurrent && batch.calls.length > 1) {
18830
+ const batchResults = await runToolBatchConcurrently(batch.calls, tools, signal, stream, limit, canUseTool);
18831
+ results.push(...batchResults);
18832
+ } else {
18833
+ const toolCall = batch.calls[0];
18834
+ telemetry.start(`tool:${toolCall.name}`);
18835
+ const toolResult = await executeToolCall(toolCall, tools, signal, stream, {
18836
+ emitMessageEvents: true,
18837
+ canUseTool
18838
+ });
18839
+ telemetry.log(`tool:${toolCall.name}`, telemetry.end(`tool:${toolCall.name}`));
18840
+ results.push(toolResult);
18841
+ }
18842
+ dispatched += batch.calls.length;
18634
18843
  if (!getSteeringMessages) {
18635
18844
  continue;
18636
18845
  }
@@ -18639,7 +18848,7 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee
18639
18848
  continue;
18640
18849
  }
18641
18850
  steeringMessages = steering;
18642
- for (const skipped of requestedCalls.slice(index + 1)) {
18851
+ for (const skipped of requestedCalls.slice(dispatched)) {
18643
18852
  results.push(skipToolCall(skipped, stream));
18644
18853
  }
18645
18854
  break;
@@ -18868,6 +19077,7 @@ var LoopConfigFactory = class {
18868
19077
  convertToLlm: args.convertToLlm,
18869
19078
  transformContext: args.transformContext,
18870
19079
  getApiKey: args.getApiKey,
19080
+ canUseTool: args.canUseTool,
18871
19081
  getSteeringMessages: args.getSteeringMessages,
18872
19082
  getFollowUpMessages: args.getFollowUpMessages
18873
19083
  };
@@ -18940,6 +19150,7 @@ var Agent = class {
18940
19150
  streamFn;
18941
19151
  _sessionId;
18942
19152
  getApiKey;
19153
+ canUseTool;
18943
19154
  runningPrompt;
18944
19155
  resolveRunningPrompt;
18945
19156
  _thinkingBudgets;
@@ -18950,6 +19161,7 @@ var Agent = class {
18950
19161
  this.streamFn = opts.streamFn || streamSimple;
18951
19162
  this._sessionId = opts.sessionId;
18952
19163
  this.getApiKey = opts.getApiKey;
19164
+ this.canUseTool = opts.canUseTool;
18953
19165
  this._thinkingBudgets = opts.thinkingBudgets;
18954
19166
  this.queuePolicy = new MessageQueuePolicy(opts.steeringMode || "one-at-a-time", opts.followUpMode || "one-at-a-time");
18955
19167
  }
@@ -19109,6 +19321,7 @@ var Agent = class {
19109
19321
  convertToLlm: this.convertToLlm,
19110
19322
  transformContext: this.transformContext,
19111
19323
  getApiKey: this.getApiKey,
19324
+ canUseTool: this.canUseTool,
19112
19325
  getSteeringMessages: async () => this.queuePolicy.dequeueSteeringMessages(),
19113
19326
  getFollowUpMessages: async () => this.queuePolicy.dequeueFollowUpMessages()
19114
19327
  });
@@ -19421,7 +19634,7 @@ function isToolCall(value) {
19421
19634
  // src/facade/bot/actions/bash.ts
19422
19635
  import { randomBytes } from "node:crypto";
19423
19636
  import { createWriteStream, existsSync as existsSync2 } from "node:fs";
19424
- import { tmpdir } from "node:os";
19637
+ import { tmpdir as tmpdir2 } from "node:os";
19425
19638
  import { join } from "node:path";
19426
19639
  import { Type as Type2 } from "@sinclair/typebox";
19427
19640
  import { spawn as spawn2 } from "child_process";
@@ -19500,6 +19713,128 @@ function killProcessTree(pid) {
19500
19713
  }
19501
19714
  }
19502
19715
 
19716
+ // src/facade/bot/actions/sandbox-backend.ts
19717
+ import { spawnSync as spawnSync2 } from "node:child_process";
19718
+ import { tmpdir } from "node:os";
19719
+ function resolveWritableRoots(config, cwd) {
19720
+ const tmp = config.tmpDir ?? tmpdir();
19721
+ const extra = config.writableRoots ?? [];
19722
+ const seen = /* @__PURE__ */ new Set();
19723
+ const roots = [];
19724
+ for (const p of [cwd, tmp, ...extra]) {
19725
+ if (p && !seen.has(p)) {
19726
+ seen.add(p);
19727
+ roots.push(p);
19728
+ }
19729
+ }
19730
+ return roots;
19731
+ }
19732
+ function escapeSeatbeltPath(p) {
19733
+ return p.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
19734
+ }
19735
+ function buildSeatbeltProfile(config, cwd) {
19736
+ const roots = resolveWritableRoots(config, cwd);
19737
+ const writeRules = roots.map((p) => `(allow file-write* (subpath "${escapeSeatbeltPath(p)}"))`).join("\n");
19738
+ const networkRule = config.allowNetwork ? "(allow network*)" : "(deny network* (with no-log))";
19739
+ return [
19740
+ "(version 1)",
19741
+ "(deny default)",
19742
+ // Allow the command and its children to run.
19743
+ "(allow process-exec)",
19744
+ "(allow process-fork)",
19745
+ "(allow signal (target same-sandbox))",
19746
+ "(allow sysctl-read)",
19747
+ // Read is permitted everywhere; the boundary we enforce is on writes.
19748
+ "(allow file-read*)",
19749
+ writeRules,
19750
+ networkRule
19751
+ ].filter((line) => line.length > 0).join("\n");
19752
+ }
19753
+ function buildSeatbeltArgv(config, cwd, command) {
19754
+ const profile = buildSeatbeltProfile(config, cwd);
19755
+ return ["sandbox-exec", "-p", profile, "/bin/sh", "-c", command];
19756
+ }
19757
+ function buildBwrapArgv(config, cwd, command) {
19758
+ const roots = resolveWritableRoots(config, cwd);
19759
+ const argv = ["bwrap", "--ro-bind", "/", "/"];
19760
+ for (const root of roots) {
19761
+ argv.push("--bind", root, root);
19762
+ }
19763
+ argv.push("--dev", "/dev", "--proc", "/proc");
19764
+ if (!config.allowNetwork) {
19765
+ argv.push("--unshare-net");
19766
+ }
19767
+ argv.push("--chdir", cwd, "/bin/sh", "-c", command);
19768
+ return argv;
19769
+ }
19770
+ function binaryOnPath(binary) {
19771
+ try {
19772
+ const result = spawnSync2("/bin/sh", ["-c", `command -v ${binary}`], {
19773
+ encoding: "utf8",
19774
+ timeout: 5e3
19775
+ });
19776
+ return result.status === 0 && typeof result.stdout === "string" && result.stdout.trim().length > 0;
19777
+ } catch {
19778
+ return false;
19779
+ }
19780
+ }
19781
+ function sandboxAvailability(platform = process.platform, probe = binaryOnPath) {
19782
+ if (platform === "darwin") {
19783
+ const binaryPresent = probe("sandbox-exec");
19784
+ return {
19785
+ platform: "macos",
19786
+ binary: "sandbox-exec",
19787
+ binaryPresent,
19788
+ reason: binaryPresent ? void 0 : "sandbox-exec not found on PATH"
19789
+ };
19790
+ }
19791
+ if (platform === "linux") {
19792
+ const binaryPresent = probe("bwrap");
19793
+ return {
19794
+ platform: "linux",
19795
+ binary: "bwrap",
19796
+ binaryPresent,
19797
+ reason: binaryPresent ? void 0 : "bwrap (bubblewrap) not found on PATH"
19798
+ };
19799
+ }
19800
+ return {
19801
+ platform: "unsupported",
19802
+ binary: null,
19803
+ binaryPresent: false,
19804
+ reason: `OS sandbox is not supported on platform "${platform}"`
19805
+ };
19806
+ }
19807
+ function buildSandboxArgv(config, cwd, command, availability = sandboxAvailability()) {
19808
+ if (!availability.binaryPresent) return null;
19809
+ if (availability.platform === "macos") return buildSeatbeltArgv(config, cwd, command);
19810
+ if (availability.platform === "linux") return buildBwrapArgv(config, cwd, command);
19811
+ return null;
19812
+ }
19813
+ function shellQuote(token) {
19814
+ return `'${token.replace(/'/g, "'\\''")}'`;
19815
+ }
19816
+ function argvToCommandString(argv) {
19817
+ return argv.map(shellQuote).join(" ");
19818
+ }
19819
+ function createSandboxedBashOperations(config, options) {
19820
+ if (!config.enabled) return options.base;
19821
+ const availability = options.availability ?? sandboxAvailability();
19822
+ return {
19823
+ exec: (command, cwd, execOptions) => {
19824
+ const argv = buildSandboxArgv(config, cwd, command, availability);
19825
+ if (!argv) {
19826
+ options.onNote?.(
19827
+ `sandbox NOT applied (${availability.reason ?? "unavailable"}); ran un-sandboxed`
19828
+ );
19829
+ return options.base.exec(command, cwd, execOptions);
19830
+ }
19831
+ options.onNote?.(`sandbox applied via ${availability.binary}`);
19832
+ const wrapped = argvToCommandString(argv);
19833
+ return options.base.exec(wrapped, cwd, execOptions);
19834
+ }
19835
+ };
19836
+ }
19837
+
19503
19838
  // src/facade/bot/actions/truncate.ts
19504
19839
  var DEFAULT_MAX_LINES = 2500;
19505
19840
  var DEFAULT_MAX_BYTES = 64 * 1024;
@@ -19687,7 +20022,7 @@ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
19687
20022
  // src/facade/bot/actions/bash.ts
19688
20023
  function allocateSpillFilePath() {
19689
20024
  const suffix = randomBytes(8).toString("hex");
19690
- return join(tmpdir(), `indusvx-bash-${suffix}.log`);
20025
+ return join(tmpdir2(), `indusvx-bash-${suffix}.log`);
19691
20026
  }
19692
20027
  var BashCommandBuilder = class {
19693
20028
  constructor(prefix) {
@@ -19864,9 +20199,15 @@ ${note}` : note);
19864
20199
  }
19865
20200
  };
19866
20201
  var DEFAULT_BLOCKED_PATTERNS = [/\brm\s+-rf\s+\/$/, /:\(\)\s*\{\s*:\|:\s*&\s*\};:/];
20202
+ var DEFAULT_BASH_TIMEOUT_SECONDS = 120;
20203
+ var MAX_BASH_TIMEOUT_SECONDS = 600;
20204
+ function computeEffectiveTimeout(requested, def, max) {
20205
+ const base = requested !== void 0 && requested > 0 ? requested : def;
20206
+ return Math.min(base, max);
20207
+ }
19867
20208
  var bashSchema = Type2.Object({
19868
20209
  command: Type2.String({ description: "The bash command line to execute" }),
19869
- timeout: Type2.Optional(Type2.Number({ description: "Optional limit, in seconds, after which the command is terminated. No limit applies when omitted." }))
20210
+ timeout: Type2.Optional(Type2.Number({ description: `Optional limit, in seconds, after which the command is terminated (default ${DEFAULT_BASH_TIMEOUT_SECONDS}s, max ${MAX_BASH_TIMEOUT_SECONDS}s).` }))
19870
20211
  });
19871
20212
  var defaultBashOperations = {
19872
20213
  exec: (command, cwd, { onData, signal, timeout, env }) => {
@@ -19932,21 +20273,28 @@ var defaultBashOperations = {
19932
20273
  }
19933
20274
  };
19934
20275
  function createBashTool(cwd, options) {
19935
- const ops = options?.operations ?? defaultBashOperations;
20276
+ const baseOps = options?.operations ?? defaultBashOperations;
20277
+ const ops = options?.sandbox?.enabled ? createSandboxedBashOperations(options.sandbox, {
20278
+ base: baseOps,
20279
+ onNote: options.onSandboxNote
20280
+ }) : baseOps;
19936
20281
  const commandBuilder = new BashCommandBuilder(options?.commandPrefix);
19937
20282
  const securityPolicy = new BashSecurityPolicy(
19938
20283
  options?.security?.blockedPatterns ?? DEFAULT_BLOCKED_PATTERNS,
19939
20284
  options?.commandHistory
19940
20285
  );
19941
20286
  const hookRunner = options?.hookRunner;
20287
+ const defaultTimeoutSeconds = options?.defaultTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS;
20288
+ const maxTimeoutSeconds = Math.max(options?.maxTimeoutSeconds ?? MAX_BASH_TIMEOUT_SECONDS, defaultTimeoutSeconds);
19942
20289
  return {
19943
20290
  name: "bash",
19944
20291
  label: "bash",
19945
- description: `Execute a bash command in the current working directory and return its merged stdout and stderr. At most the final ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB are returned, whichever limit is hit first; once that occurs the complete output is saved to a temp file. An optional timeout in seconds may be supplied.`,
20292
+ description: `Execute a bash command in the current working directory and return its merged stdout and stderr. At most the final ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB are returned, whichever limit is hit first; once that occurs the complete output is saved to a temp file. An optional timeout in seconds may be supplied. Commands time out after ${defaultTimeoutSeconds}s by default (max ${maxTimeoutSeconds}s).`,
19946
20293
  parameters: bashSchema,
19947
20294
  execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
19948
20295
  securityPolicy.assertAllowed(command);
19949
20296
  securityPolicy.record(command);
20297
+ const effectiveTimeout = computeEffectiveTimeout(timeout, defaultTimeoutSeconds, maxTimeoutSeconds);
19950
20298
  const resolvedCommand = commandBuilder.build(command);
19951
20299
  const hookEnv = hookRunner?.hasHandlers("shell.env") ? (await hookRunner.trigger("shell.env", { cwd }, { env: {} })).env : void 0;
19952
20300
  const safeEnv = hookEnv ? Object.fromEntries(
@@ -19958,7 +20306,7 @@ function createBashTool(cwd, options) {
19958
20306
  ops.exec(resolvedCommand, cwd, {
19959
20307
  onData: (data) => outputCapture.appendChunk(data, emitPartial),
19960
20308
  signal,
19961
- timeout,
20309
+ timeout: effectiveTimeout,
19962
20310
  env: safeEnv
19963
20311
  }).then(({ exitCode }) => {
19964
20312
  outputCapture.close();
@@ -20291,6 +20639,216 @@ async function computeEditDiff(path9, oldText, newText, cwd) {
20291
20639
  return computation.compute(path9, oldText, newText);
20292
20640
  }
20293
20641
 
20642
+ // src/facade/bot/actions/edit-utils.ts
20643
+ import { readdirSync } from "node:fs";
20644
+ import { realpath, stat } from "node:fs/promises";
20645
+ import { basename, dirname, extname, join as join2, relative, sep } from "node:path";
20646
+ var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
20647
+ var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
20648
+ var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
20649
+ var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
20650
+ function isOpeningContext(chars, index) {
20651
+ if (index === 0) {
20652
+ return true;
20653
+ }
20654
+ const prev = chars[index - 1];
20655
+ return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || // em dash
20656
+ prev === "\u2013";
20657
+ }
20658
+ function applyCurlyDoubleQuotes(str) {
20659
+ const chars = [...str];
20660
+ const result = [];
20661
+ for (let i = 0; i < chars.length; i++) {
20662
+ if (chars[i] === '"') {
20663
+ result.push(isOpeningContext(chars, i) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE);
20664
+ } else {
20665
+ result.push(chars[i]);
20666
+ }
20667
+ }
20668
+ return result.join("");
20669
+ }
20670
+ function applyCurlySingleQuotes(str) {
20671
+ const chars = [...str];
20672
+ const result = [];
20673
+ for (let i = 0; i < chars.length; i++) {
20674
+ if (chars[i] === "'") {
20675
+ const prev = i > 0 ? chars[i - 1] : void 0;
20676
+ const next = i < chars.length - 1 ? chars[i + 1] : void 0;
20677
+ const prevIsLetter = prev !== void 0 && new RegExp("\\p{L}", "u").test(prev);
20678
+ const nextIsLetter = next !== void 0 && new RegExp("\\p{L}", "u").test(next);
20679
+ if (prevIsLetter && nextIsLetter) {
20680
+ result.push(RIGHT_SINGLE_CURLY_QUOTE);
20681
+ } else {
20682
+ result.push(isOpeningContext(chars, i) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE);
20683
+ }
20684
+ } else {
20685
+ result.push(chars[i]);
20686
+ }
20687
+ }
20688
+ return result.join("");
20689
+ }
20690
+ function preserveQuoteStyle(oldString, actualOldString, newString) {
20691
+ if (oldString === actualOldString) {
20692
+ return newString;
20693
+ }
20694
+ const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
20695
+ const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
20696
+ if (!hasDoubleQuotes && !hasSingleQuotes) {
20697
+ return newString;
20698
+ }
20699
+ let result = newString;
20700
+ if (hasDoubleQuotes) {
20701
+ result = applyCurlyDoubleQuotes(result);
20702
+ }
20703
+ if (hasSingleQuotes) {
20704
+ result = applyCurlySingleQuotes(result);
20705
+ }
20706
+ return result;
20707
+ }
20708
+ var DESANITIZATIONS = {
20709
+ "<fnr>": "<function_results>",
20710
+ "<n>": "<name>",
20711
+ "</n>": "</name>",
20712
+ "<o>": "<output>",
20713
+ "</o>": "</output>",
20714
+ "<e>": "<error>",
20715
+ "</e>": "</error>",
20716
+ "<s>": "<system>",
20717
+ "</s>": "</system>",
20718
+ "<r>": "<result>",
20719
+ "</r>": "</result>",
20720
+ "< META_START >": "<META_START>",
20721
+ "< META_END >": "<META_END>",
20722
+ "< EOT >": "<EOT>",
20723
+ "< META >": "<META>",
20724
+ "< SOS >": "<SOS>",
20725
+ "\n\nH:": "\n\nHuman:",
20726
+ "\n\nA:": "\n\nAssistant:"
20727
+ };
20728
+ function desanitizeMatchString(matchString) {
20729
+ let result = matchString;
20730
+ const appliedReplacements = [];
20731
+ for (const [from, to] of Object.entries(DESANITIZATIONS)) {
20732
+ const beforeReplace = result;
20733
+ result = result.split(from).join(to);
20734
+ if (beforeReplace !== result) {
20735
+ appliedReplacements.push({ from, to });
20736
+ }
20737
+ }
20738
+ return { result, appliedReplacements };
20739
+ }
20740
+ function findSimilarFile(filePath) {
20741
+ try {
20742
+ const dir = dirname(filePath);
20743
+ const fileBaseName = basename(filePath, extname(filePath));
20744
+ const files = readdirSync(dir, { withFileTypes: true });
20745
+ const similarFiles = files.filter(
20746
+ (file) => basename(file.name, extname(file.name)) === fileBaseName && join2(dir, file.name) !== filePath
20747
+ );
20748
+ const firstMatch = similarFiles[0];
20749
+ return firstMatch ? firstMatch.name : void 0;
20750
+ } catch {
20751
+ return void 0;
20752
+ }
20753
+ }
20754
+ var FILE_NOT_FOUND_CWD_NOTE = "Note: your current working directory is";
20755
+ async function suggestPathUnderCwd(requestedPath, cwd) {
20756
+ const cwdParent = dirname(cwd);
20757
+ let resolvedPath = requestedPath;
20758
+ try {
20759
+ const resolvedDir = await realpath(dirname(requestedPath));
20760
+ resolvedPath = join2(resolvedDir, basename(requestedPath));
20761
+ } catch {
20762
+ }
20763
+ const cwdParentPrefix = cwdParent === sep ? sep : cwdParent + sep;
20764
+ if (!resolvedPath.startsWith(cwdParentPrefix) || resolvedPath.startsWith(cwd + sep) || resolvedPath === cwd) {
20765
+ return void 0;
20766
+ }
20767
+ const relFromParent = relative(cwdParent, resolvedPath);
20768
+ const correctedPath = join2(cwd, relFromParent);
20769
+ try {
20770
+ await stat(correctedPath);
20771
+ return correctedPath;
20772
+ } catch {
20773
+ return void 0;
20774
+ }
20775
+ }
20776
+ function replaceAllLiteral(haystack, needle, value) {
20777
+ if (needle.length === 0) return haystack;
20778
+ const pieces = [];
20779
+ let from = 0;
20780
+ for (; ; ) {
20781
+ const at = haystack.indexOf(needle, from);
20782
+ if (at === -1) {
20783
+ pieces.push(haystack.slice(from));
20784
+ break;
20785
+ }
20786
+ pieces.push(haystack.slice(from, at), value);
20787
+ from = at + needle.length;
20788
+ }
20789
+ return pieces.join("");
20790
+ }
20791
+
20792
+ // src/facade/bot/actions/checkpoint.ts
20793
+ import { readFileSync, statSync } from "node:fs";
20794
+ function recordCheckpoint(handle, absPath) {
20795
+ if (!handle) return;
20796
+ let previous;
20797
+ try {
20798
+ statSync(absPath);
20799
+ previous = readFileSync(absPath, "utf-8");
20800
+ } catch (error) {
20801
+ if (error?.code === "ENOENT") {
20802
+ previous = null;
20803
+ } else {
20804
+ return;
20805
+ }
20806
+ }
20807
+ try {
20808
+ handle.record(absPath, previous);
20809
+ } catch {
20810
+ }
20811
+ }
20812
+
20813
+ // src/facade/bot/actions/read-state.ts
20814
+ import { statSync as statSync2 } from "node:fs";
20815
+ var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
20816
+ var MODIFIED_SINCE_READ_MESSAGE = "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
20817
+ function recordReadState(handle, absPath) {
20818
+ if (!handle) return;
20819
+ try {
20820
+ const info = statSync2(absPath);
20821
+ const mtimeMs = Math.floor(info.mtimeMs);
20822
+ handle.set(absPath, {
20823
+ mtimeMs,
20824
+ size: info.size,
20825
+ readAt: mtimeMs
20826
+ });
20827
+ } catch {
20828
+ }
20829
+ }
20830
+ function enforceReadGate(handle, absPath) {
20831
+ if (!handle) return { ok: true };
20832
+ if (!handle.has(absPath)) {
20833
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
20834
+ }
20835
+ const recorded = handle.get(absPath);
20836
+ if (!recorded) {
20837
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
20838
+ }
20839
+ let info;
20840
+ try {
20841
+ info = statSync2(absPath);
20842
+ } catch {
20843
+ return { ok: true };
20844
+ }
20845
+ const currentMtime = Math.floor(info.mtimeMs);
20846
+ if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
20847
+ return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
20848
+ }
20849
+ return { ok: true };
20850
+ }
20851
+
20294
20852
  // src/facade/bot/actions/edit.ts
20295
20853
  var ExactThenFuzzyStrategy = class {
20296
20854
  find(content, oldText) {
@@ -20311,7 +20869,12 @@ function rejectWith(message) {
20311
20869
  var editSchema = Type3.Object({
20312
20870
  path: Type3.String({ description: "File to modify, given as a relative or absolute path" }),
20313
20871
  oldText: Type3.String({ description: "The current text to locate; it must be reproduced verbatim" }),
20314
- newText: Type3.String({ description: "The text that should take the place of oldText" })
20872
+ newText: Type3.String({ description: "The text that should take the place of oldText" }),
20873
+ replaceAll: Type3.Optional(
20874
+ Type3.Boolean({
20875
+ description: "Replace every occurrence of oldText instead of requiring a single unique match. Defaults to false, which refuses the edit when oldText appears more than once."
20876
+ })
20877
+ )
20315
20878
  });
20316
20879
  var defaultEditOperations = {
20317
20880
  readFile: (path9) => fsReadFile(path9),
@@ -20353,38 +20916,76 @@ function withAbort(signal, task) {
20353
20916
  });
20354
20917
  }
20355
20918
  var EditSession = class {
20356
- constructor(input, absolutePath, ops, matchingStrategy) {
20919
+ constructor(input, absolutePath, ops, matchingStrategy, cwd, readState, checkpoint) {
20357
20920
  this.input = input;
20358
20921
  this.absolutePath = absolutePath;
20359
20922
  this.ops = ops;
20360
20923
  this.matchingStrategy = matchingStrategy;
20924
+ this.cwd = cwd;
20925
+ this.readState = readState;
20926
+ this.checkpoint = checkpoint;
20927
+ this.replaceAll = input.replaceAll === true;
20361
20928
  }
20362
20929
  input;
20363
20930
  absolutePath;
20364
20931
  ops;
20365
20932
  matchingStrategy;
20933
+ cwd;
20934
+ readState;
20935
+ checkpoint;
20936
+ /** Whether every occurrence should be swapped (vs. requiring a unique hit). */
20937
+ replaceAll;
20366
20938
  async run(throwIfAborted2) {
20367
20939
  throwIfAborted2();
20940
+ const gate = enforceReadGate(this.readState, this.absolutePath);
20941
+ if (!gate.ok) {
20942
+ rejectWith(gate.message);
20943
+ }
20944
+ recordCheckpoint(this.checkpoint, this.absolutePath);
20368
20945
  const { sourceText } = await this.loadSource();
20369
20946
  throwIfAborted2();
20370
20947
  const normalized = this.normalizeInputs(sourceText);
20371
- const match = this.locateUniqueMatch(normalized);
20372
- const replacement = this.applyReplacement(normalized, match);
20948
+ const search = this.resolveSearch(normalized);
20949
+ const match = this.locateUniqueMatch(normalized, search);
20950
+ const replacement = this.applyReplacement(normalized, search, match);
20373
20951
  throwIfAborted2();
20374
20952
  await this.persist(replacement.finalContent);
20375
20953
  throwIfAborted2();
20376
- return this.buildResponse(replacement.editableBase, replacement.replacedContent);
20954
+ return this.buildResponse(replacement.editableBase, replacement.replacedContent, replacement.replacements);
20377
20955
  }
20378
20956
  async loadSource() {
20379
20957
  try {
20380
20958
  await this.ops.access(this.absolutePath);
20381
20959
  } catch {
20382
- rejectWith(`No readable file at ${this.input.path}.`);
20960
+ rejectWith(await this.buildNotFoundMessage());
20383
20961
  }
20384
20962
  const buffer = await this.ops.readFile(this.absolutePath);
20385
20963
  const sourceText = buffer.toString("utf-8");
20386
20964
  return { sourceText };
20387
20965
  }
20966
+ /**
20967
+ * Compose the "No readable file …" rejection, enriched with a "Did you mean
20968
+ * …?" hint when we can spot either a sibling file sharing the base name or a
20969
+ * corrected path re-rooted under the working directory. Falls back to the
20970
+ * bare message when no suggestion applies, so the default behaviour is
20971
+ * preserved for the common case.
20972
+ */
20973
+ async buildNotFoundMessage() {
20974
+ let message = `No readable file at ${this.input.path}. ${FILE_NOT_FOUND_CWD_NOTE} ${this.cwd}.`;
20975
+ try {
20976
+ const cwdSuggestion = await suggestPathUnderCwd(this.absolutePath, this.cwd);
20977
+ if (cwdSuggestion) {
20978
+ message += ` Did you mean ${cwdSuggestion}?`;
20979
+ return message;
20980
+ }
20981
+ const similarFilename = findSimilarFile(this.absolutePath);
20982
+ if (similarFilename) {
20983
+ message += ` Did you mean ${similarFilename}?`;
20984
+ }
20985
+ } catch {
20986
+ }
20987
+ return message;
20988
+ }
20388
20989
  normalizeInputs(sourceText) {
20389
20990
  const { bom, text } = stripBom(sourceText);
20390
20991
  const originalEnding = detectLineEnding(text);
@@ -20399,41 +21000,106 @@ var EditSession = class {
20399
21000
  normalizedNewText
20400
21001
  };
20401
21002
  }
20402
- locateUniqueMatch(normalized) {
20403
- const matchResult = this.matchingStrategy.find(normalized.normalizedContent, normalized.normalizedOldText);
21003
+ /**
21004
+ * Decide which oldText/newText pair to actually edit with. The literal
21005
+ * normalized inputs win whenever they appear in the file; only when they are
21006
+ * absent do we try a de-sanitized variant (repairing mangled tag tokens like
21007
+ * `<o>` → `<output>`), mirroring the same token expansions onto newText so
21008
+ * the replacement stays consistent. The de-sanitize table overlaps ordinary
21009
+ * code, so it is deliberately a *fallback*, never the first attempt.
21010
+ */
21011
+ resolveSearch(normalized) {
21012
+ const literal = {
21013
+ searchOldText: normalized.normalizedOldText,
21014
+ searchNewText: normalized.normalizedNewText
21015
+ };
21016
+ if (this.matchingStrategy.find(normalized.normalizedContent, normalized.normalizedOldText).found) {
21017
+ return literal;
21018
+ }
21019
+ const { result: desanitizedOldText, appliedReplacements } = desanitizeMatchString(
21020
+ normalized.normalizedOldText
21021
+ );
21022
+ if (appliedReplacements.length === 0) {
21023
+ return literal;
21024
+ }
21025
+ if (!this.matchingStrategy.find(normalized.normalizedContent, desanitizedOldText).found) {
21026
+ return literal;
21027
+ }
21028
+ let desanitizedNewText = normalized.normalizedNewText;
21029
+ for (const { from, to } of appliedReplacements) {
21030
+ desanitizedNewText = desanitizedNewText.split(from).join(to);
21031
+ }
21032
+ return { searchOldText: desanitizedOldText, searchNewText: desanitizedNewText };
21033
+ }
21034
+ locateUniqueMatch(normalized, search) {
21035
+ const matchResult = this.matchingStrategy.find(normalized.normalizedContent, search.searchOldText);
20404
21036
  if (!matchResult.found) {
20405
21037
  rejectWith(
20406
21038
  `The supplied text was not located in ${this.input.path}. It has to line up character for character, including every space and line break.`
20407
21039
  );
20408
21040
  }
20409
21041
  const fuzzyContent = normalizeForFuzzyMatch(normalized.normalizedContent);
20410
- const fuzzyOldText = normalizeForFuzzyMatch(normalized.normalizedOldText);
21042
+ const fuzzyOldText = normalizeForFuzzyMatch(search.searchOldText);
20411
21043
  const matchCount = fuzzyContent.split(fuzzyOldText).length - 1;
20412
- if (matchCount > 1) {
21044
+ if (matchCount > 1 && !this.replaceAll) {
20413
21045
  rejectWith(
20414
- `The text appears ${matchCount} times in ${this.input.path}, so the target is ambiguous. Include surrounding lines so it matches in exactly one place.`
21046
+ `The text appears ${matchCount} times in ${this.input.path}, so the target is ambiguous. Include surrounding lines so it matches in exactly one place, or set replaceAll to true to change every occurrence.`
20415
21047
  );
20416
21048
  }
20417
21049
  return { matchResult, matchCount };
20418
21050
  }
20419
- applyReplacement(normalized, match) {
20420
- const editableBase = match.matchResult.contentForReplacement;
20421
- const replacedContent = editableBase.substring(0, match.matchResult.index) + normalized.normalizedNewText + editableBase.substring(match.matchResult.index + match.matchResult.matchLength);
21051
+ applyReplacement(normalized, search, match) {
21052
+ const usedFuzzy = match.matchResult.usedFuzzyMatch;
21053
+ const matchedNeedle = usedFuzzy ? this.recoverOriginalSpan(normalized.normalizedContent, search.searchOldText) : search.searchOldText;
21054
+ const editableBase = normalized.normalizedContent;
21055
+ const replacementText = usedFuzzy ? preserveQuoteStyle(search.searchOldText, matchedNeedle, search.searchNewText) : search.searchNewText;
21056
+ const firstIndex = editableBase.indexOf(matchedNeedle);
21057
+ if (firstIndex === -1) {
21058
+ rejectWith(
21059
+ `The supplied text was not located in ${this.input.path}. It has to line up character for character, including every space and line break.`
21060
+ );
21061
+ }
21062
+ const replacedContent = this.replaceAll ? replaceAllLiteral(editableBase, matchedNeedle, replacementText) : editableBase.substring(0, firstIndex) + replacementText + editableBase.substring(firstIndex + matchedNeedle.length);
20422
21063
  if (editableBase === replacedContent) {
20423
21064
  rejectWith(
20424
21065
  `The edit left ${this.input.path} unchanged because the replacement matches the original. Often this points to unexpected special characters or text that is not actually present.`
20425
21066
  );
20426
21067
  }
21068
+ const replacements = this.replaceAll ? Math.max(match.matchCount, 1) : 1;
20427
21069
  const finalContent = normalized.bom + restoreLineEndings(replacedContent, normalized.originalEnding);
20428
- return { editableBase, replacedContent, finalContent };
21070
+ return { editableBase, replacedContent, finalContent, replacements };
21071
+ }
21072
+ /**
21073
+ * Recover the original (un-folded) span in `content` whose glyph-folded form
21074
+ * equals the folded `searchOldText`. Mirrors induscode's `findActualString`:
21075
+ * locate the match in fully-folded space, then slice the same window out of
21076
+ * the original content so curly quotes and other glyphs are preserved. Falls
21077
+ * back to the supplied text if the window can't be recovered.
21078
+ */
21079
+ recoverOriginalSpan(content, searchOldText) {
21080
+ const foldedContent = normalizeForFuzzyMatch(content);
21081
+ const foldedNeedle = normalizeForFuzzyMatch(searchOldText);
21082
+ const at = foldedContent.indexOf(foldedNeedle);
21083
+ if (at === -1) {
21084
+ return searchOldText;
21085
+ }
21086
+ for (let len = foldedNeedle.length; at + len <= content.length; len++) {
21087
+ const candidate = content.slice(at, at + len);
21088
+ if (normalizeForFuzzyMatch(candidate) === foldedNeedle) {
21089
+ return candidate;
21090
+ }
21091
+ }
21092
+ return content.slice(at, at + foldedNeedle.length);
20429
21093
  }
20430
21094
  async persist(finalContent) {
20431
21095
  await this.ops.writeFile(this.absolutePath, finalContent);
21096
+ recordReadState(this.readState, this.absolutePath);
20432
21097
  }
20433
- buildResponse(editableBase, replacedContent) {
21098
+ buildResponse(editableBase, replacedContent, replacements) {
20434
21099
  const diffResult = generateDiffString(editableBase, replacedContent);
21100
+ const summary = replacements > 1 ? `Replaced the target text in ${this.input.path} (${replacements} occurrences).` : `Replaced the target text in ${this.input.path}.`;
20435
21101
  return {
20436
- content: [{ type: "text", text: `Replaced the target text in ${this.input.path}.` }],
21102
+ content: [{ type: "text", text: summary }],
20437
21103
  details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
20438
21104
  };
20439
21105
  }
@@ -20451,12 +21117,20 @@ function createEditTool(cwd, options) {
20451
21117
  return {
20452
21118
  name: "edit",
20453
21119
  label: "edit",
20454
- description: "Make a targeted change to a file by swapping one exact span of text for another. Provide oldText that reproduces the existing content character for character, whitespace included. Best for narrow, precise edits.",
21120
+ description: "Make a targeted change to a file by swapping one exact span of text for another. Provide oldText that reproduces the existing content character for character, whitespace included. Best for narrow, precise edits. By default oldText must match exactly once; set replaceAll to true to change every occurrence instead.",
20455
21121
  parameters: editSchema,
20456
21122
  execute: async (_toolCallId, input, signal) => {
20457
21123
  validator.validate(input.path, input.oldText);
20458
21124
  const absolutePath = resolveToCwd(input.path, cwd);
20459
- const session = new EditSession(input, absolutePath, ops, matchingStrategy);
21125
+ const session = new EditSession(
21126
+ input,
21127
+ absolutePath,
21128
+ ops,
21129
+ matchingStrategy,
21130
+ cwd,
21131
+ options?.readState,
21132
+ options?.checkpoint
21133
+ );
20460
21134
  return withAbort(signal, async ({ throwIfAborted: throwIfAborted2 }) => session.run(throwIfAborted2));
20461
21135
  }
20462
21136
  };
@@ -20465,28 +21139,96 @@ var editTool = createEditTool(process.cwd());
20465
21139
 
20466
21140
  // src/facade/bot/actions/find.ts
20467
21141
  import { Type as Type4 } from "@sinclair/typebox";
20468
- import { existsSync as existsSync3 } from "fs";
21142
+ import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
20469
21143
  import path from "path";
20470
21144
  var findSchema = Type4.Object({
20471
21145
  pattern: Type4.String({
20472
21146
  description: 'Glob that the filenames should match, such as "*.ts", "**/*.json", or "lib/**/*.test.ts"'
20473
21147
  }),
20474
21148
  path: Type4.Optional(Type4.String({ description: "Directory tree to traverse. Falls back to the working directory." })),
21149
+ type: Type4.Optional(
21150
+ Type4.String({
21151
+ description: `Restrict results to a language/category (for example "ts", "js", "py"). Filters by the entry's file extension. Defaults to no type filter.`
21152
+ })
21153
+ ),
20475
21154
  limit: Type4.Optional(Type4.Number({ description: "Maximum count of paths handed back. Defaults to 1024." }))
20476
21155
  });
20477
21156
  var DEFAULT_LIMIT = 1024;
20478
21157
  var DEFAULT_EXCLUDES = ["node_modules", ".git"];
20479
21158
  var MAX_DIRECTORY_DEPTH = 20;
21159
+ function escapeRegexLiteral(literal) {
21160
+ return literal.replace(/[\\.*+?^${}()|[\]]/g, "\\$&");
21161
+ }
21162
+ function compileGlob(pattern) {
21163
+ let body = "";
21164
+ for (let i = 0; i < pattern.length; i++) {
21165
+ const ch = pattern[i];
21166
+ if (ch === "*") {
21167
+ if (pattern[i + 1] === "*") {
21168
+ i++;
21169
+ if (pattern[i + 1] === "/") {
21170
+ i++;
21171
+ body += "(?:[^/]*/)*";
21172
+ } else {
21173
+ body += ".*";
21174
+ }
21175
+ } else {
21176
+ body += "[^/]*";
21177
+ }
21178
+ } else if (ch === "?") {
21179
+ body += "[^/]";
21180
+ } else {
21181
+ body += escapeRegexLiteral(ch);
21182
+ }
21183
+ }
21184
+ try {
21185
+ return new RegExp(`^${body}$`);
21186
+ } catch {
21187
+ return null;
21188
+ }
21189
+ }
20480
21190
  var GlobMatcher = class {
20481
- regex;
21191
+ relRegex;
21192
+ baseRegex;
21193
+ hasSeparator;
20482
21194
  constructor(pattern) {
20483
- const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".").replace(/\./g, "\\.");
20484
- this.regex = new RegExp(`^${regexPattern}$`);
20485
- }
20486
- matches(value) {
20487
- return this.regex.test(value);
21195
+ this.hasSeparator = pattern.includes("/");
21196
+ this.relRegex = compileGlob(pattern);
21197
+ this.baseRegex = this.hasSeparator ? null : this.relRegex;
21198
+ }
21199
+ /** Test against the root-relative path (preferred), falling back to basename. */
21200
+ matchesPath(relativePath) {
21201
+ const normalized = relativePath.split(path.sep).join("/");
21202
+ if (this.relRegex && this.relRegex.test(normalized)) {
21203
+ return true;
21204
+ }
21205
+ if (this.baseRegex) {
21206
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
21207
+ return this.baseRegex.test(base);
21208
+ }
21209
+ return false;
20488
21210
  }
20489
21211
  };
21212
+ function typeToExtensions(type) {
21213
+ const key = type.trim().toLowerCase();
21214
+ if (key.length === 0) return null;
21215
+ const TABLE = {
21216
+ ts: ["ts", "tsx", "mts", "cts"],
21217
+ js: ["js", "jsx", "mjs", "cjs"],
21218
+ py: ["py", "pyi"],
21219
+ rs: ["rs"],
21220
+ go: ["go"],
21221
+ java: ["java"],
21222
+ c: ["c", "h"],
21223
+ cpp: ["cpp", "cc", "cxx", "hpp", "hh"],
21224
+ md: ["md", "markdown"],
21225
+ json: ["json"],
21226
+ yaml: ["yaml", "yml"],
21227
+ sh: ["sh", "bash", "zsh"]
21228
+ };
21229
+ const exts = TABLE[key] ?? [key];
21230
+ return new Set(exts.map((e) => e.toLowerCase()));
21231
+ }
20490
21232
  var DirectoryWalkPolicy = class {
20491
21233
  constructor(limit) {
20492
21234
  this.limit = limit;
@@ -20500,39 +21242,50 @@ var DirectoryWalkPolicy = class {
20500
21242
  }
20501
21243
  };
20502
21244
  var DirectoryFinder = class {
20503
- constructor(matcher, walkPolicy, limit) {
21245
+ constructor(matcher, walkPolicy, limit, root, typeExtensions) {
20504
21246
  this.matcher = matcher;
20505
21247
  this.walkPolicy = walkPolicy;
20506
21248
  this.limit = limit;
21249
+ this.root = root;
21250
+ this.typeExtensions = typeExtensions;
20507
21251
  }
20508
21252
  matcher;
20509
21253
  walkPolicy;
20510
21254
  limit;
20511
- fs = __require("fs");
21255
+ root;
21256
+ typeExtensions;
20512
21257
  async find(dir) {
20513
21258
  const results = [];
20514
21259
  await this.searchRecursive(dir, 0, results);
20515
21260
  return results;
20516
21261
  }
21262
+ extensionOf(name) {
21263
+ const dot = name.lastIndexOf(".");
21264
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
21265
+ }
20517
21266
  async searchRecursive(currentDir, depth, results) {
20518
21267
  if (!this.walkPolicy.canContinue(results.length, depth)) {
20519
21268
  return;
20520
21269
  }
20521
21270
  try {
20522
- const entries = this.fs.readdirSync(currentDir);
21271
+ const entries = readdirSync2(currentDir);
20523
21272
  for (const entry of entries) {
20524
21273
  if (results.length >= this.limit) {
20525
21274
  break;
20526
21275
  }
20527
21276
  const fullPath = path.join(currentDir, entry);
20528
- const stat2 = this.fs.statSync(fullPath);
20529
- if (stat2.isDirectory()) {
21277
+ const stat3 = statSync3(fullPath);
21278
+ if (stat3.isDirectory()) {
20530
21279
  if (this.walkPolicy.shouldDescend(entry)) {
20531
21280
  await this.searchRecursive(fullPath, depth + 1, results);
20532
21281
  }
20533
21282
  continue;
20534
21283
  }
20535
- if (this.matcher.matches(entry)) {
21284
+ if (this.typeExtensions && !this.typeExtensions.has(this.extensionOf(entry))) {
21285
+ continue;
21286
+ }
21287
+ const relativePath = path.relative(this.root, fullPath);
21288
+ if (this.matcher.matchesPath(relativePath)) {
20536
21289
  results.push(fullPath);
20537
21290
  }
20538
21291
  }
@@ -20587,9 +21340,10 @@ function createFindTool(cwd, options) {
20587
21340
  return {
20588
21341
  name: "find",
20589
21342
  label: "find",
21343
+ readOnly: true,
20590
21344
  description: `Find files whose names match a glob, returning each result as a path relative to the directory that was searched. The listing halts at ${DEFAULT_LIMIT} paths or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first.`,
20591
21345
  parameters: findSchema,
20592
- execute: async (_toolCallId, { pattern, path: searchDir, limit }, signal) => {
21346
+ execute: async (_toolCallId, { pattern, path: searchDir, type, limit }, signal) => {
20593
21347
  return new Promise((resolve5, reject) => {
20594
21348
  if (signal?.aborted) {
20595
21349
  reject(new Error("Operation aborted"));
@@ -20608,7 +21362,8 @@ function createFindTool(cwd, options) {
20608
21362
  }
20609
21363
  const matcher = new GlobMatcher(pattern);
20610
21364
  const walkPolicy = new DirectoryWalkPolicy(effectiveLimit);
20611
- const finder = new DirectoryFinder(matcher, walkPolicy, effectiveLimit);
21365
+ const typeExtensions = type ? typeToExtensions(type) : null;
21366
+ const finder = new DirectoryFinder(matcher, walkPolicy, effectiveLimit, searchPath, typeExtensions);
20612
21367
  let results = await finder.find(searchPath);
20613
21368
  results = results.filter(
20614
21369
  (entryPath) => ![...excludes].some((excluded) => entryPath.includes(`/${excluded}/`) || entryPath.endsWith(`/${excluded}`))
@@ -20634,7 +21389,7 @@ var findTool = createFindTool(process.cwd());
20634
21389
 
20635
21390
  // src/facade/bot/actions/grep.ts
20636
21391
  import { Type as Type5 } from "@sinclair/typebox";
20637
- import { readFileSync, statSync } from "fs";
21392
+ import { readFileSync as readFileSync2, statSync as statSync4, readdirSync as readdirSync3 } from "fs";
20638
21393
  import path2 from "path";
20639
21394
  var grepSchema = Type5.Object({
20640
21395
  pattern: Type5.String({ description: "What to search for, given either as a regular expression or as ordinary text" }),
@@ -20644,20 +21399,158 @@ var grepSchema = Type5.Object({
20644
21399
  Type5.Boolean({ description: "Set to true to treat the pattern as exact text instead of a regex. Defaults to regex mode." })
20645
21400
  ),
20646
21401
  context: Type5.Optional(
20647
- Type5.Number({ description: "Number of neighboring lines to print above and below each hit. Defaults to zero." })
21402
+ Type5.Number({ description: "Number of neighboring lines to print above and below each hit (content mode). Defaults to zero." })
21403
+ ),
21404
+ before: Type5.Optional(
21405
+ Type5.Number({ description: "Number of lines to print BEFORE each hit (-B). Overrides `context` for leading lines." })
21406
+ ),
21407
+ after: Type5.Optional(
21408
+ Type5.Number({ description: "Number of lines to print AFTER each hit (-A). Overrides `context` for trailing lines." })
21409
+ ),
21410
+ output_mode: Type5.Optional(
21411
+ Type5.Union(
21412
+ [Type5.Literal("content"), Type5.Literal("files_with_matches"), Type5.Literal("count")],
21413
+ {
21414
+ description: 'How results are reported: "content" (default) shows matching lines, "files_with_matches" lists matching file paths, "count" reports per-file occurrence totals.'
21415
+ }
21416
+ )
21417
+ ),
21418
+ glob: Type5.Optional(
21419
+ Type5.String({
21420
+ description: 'Only search files whose root-relative path matches this glob (e.g. "*.ts" or "src/**/*.test.ts"). Multiple globs may be comma-separated. Defaults to no glob filter.'
21421
+ })
21422
+ ),
21423
+ type: Type5.Optional(
21424
+ Type5.String({
21425
+ description: 'Only search files of this language/category (for example "ts", "js", "py"), matched by extension. Defaults to no type filter.'
21426
+ })
20648
21427
  ),
20649
21428
  limit: Type5.Optional(Type5.Number({ description: "Upper bound on the count of hits returned. Defaults to 128." }))
20650
21429
  });
20651
21430
  var DEFAULT_LIMIT2 = 128;
20652
21431
  var regexCache = /* @__PURE__ */ new Map();
20653
21432
  var REGEX_CACHE_MAX_SIZE = 256;
20654
- var defaultGrepOperations = {
20655
- isDirectory: (p) => statSync(p).isDirectory(),
20656
- readFile: (p) => readFileSync(p, "utf-8"),
20657
- readdir: (p) => {
20658
- const fs8 = __require("fs");
20659
- return fs8.readdirSync(p);
21433
+ var PRUNED_DIRS = /* @__PURE__ */ new Set([".git", ".svn", ".hg", ".bzr", ".jj", ".sl", "node_modules"]);
21434
+ function escapeGlobLiteral(literal) {
21435
+ return literal.replace(/[\\.*+?^${}()|[\]]/g, "\\$&");
21436
+ }
21437
+ function compileGlob2(glob) {
21438
+ let body = "";
21439
+ for (let i = 0; i < glob.length; i++) {
21440
+ const ch = glob[i];
21441
+ if (ch === "*") {
21442
+ if (glob[i + 1] === "*") {
21443
+ i++;
21444
+ if (glob[i + 1] === "/") {
21445
+ i++;
21446
+ body += "(?:[^/]*/)*";
21447
+ } else {
21448
+ body += ".*";
21449
+ }
21450
+ } else {
21451
+ body += "[^/]*";
21452
+ }
21453
+ } else if (ch === "?") {
21454
+ body += "[^/]";
21455
+ } else {
21456
+ body += escapeGlobLiteral(ch);
21457
+ }
21458
+ }
21459
+ try {
21460
+ return new RegExp(`^${body}$`);
21461
+ } catch {
21462
+ return null;
21463
+ }
21464
+ }
21465
+ function buildIncludePredicate(glob, type) {
21466
+ const globRegexes = [];
21467
+ if (glob && glob.trim().length > 0) {
21468
+ for (const part of glob.split(",")) {
21469
+ const trimmed = part.trim();
21470
+ if (trimmed.length === 0) continue;
21471
+ const re = compileGlob2(trimmed);
21472
+ if (re) globRegexes.push(re);
21473
+ }
21474
+ }
21475
+ const extensions = type ? typeToExtensions2(type) : null;
21476
+ if (globRegexes.length === 0 && !extensions) return null;
21477
+ return (relPath) => {
21478
+ const normalized = relPath.split(path2.sep).join("/");
21479
+ if (globRegexes.length > 0) {
21480
+ const matchesGlob = globRegexes.some((re) => {
21481
+ if (re.test(normalized)) return true;
21482
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
21483
+ return re.test(base);
21484
+ });
21485
+ if (!matchesGlob) return false;
21486
+ }
21487
+ if (extensions) {
21488
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
21489
+ const dot = base.lastIndexOf(".");
21490
+ const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
21491
+ if (!extensions.has(ext)) return false;
21492
+ }
21493
+ return true;
21494
+ };
21495
+ }
21496
+ function typeToExtensions2(type) {
21497
+ const key = type.trim().toLowerCase();
21498
+ if (key.length === 0) return null;
21499
+ const TABLE = {
21500
+ ts: ["ts", "tsx", "mts", "cts"],
21501
+ js: ["js", "jsx", "mjs", "cjs"],
21502
+ py: ["py", "pyi"],
21503
+ rs: ["rs"],
21504
+ go: ["go"],
21505
+ java: ["java"],
21506
+ c: ["c", "h"],
21507
+ cpp: ["cpp", "cc", "cxx", "hpp", "hh"],
21508
+ md: ["md", "markdown"],
21509
+ json: ["json"],
21510
+ yaml: ["yaml", "yml"],
21511
+ sh: ["sh", "bash", "zsh"]
21512
+ };
21513
+ const exts = TABLE[key] ?? [key];
21514
+ return new Set(exts.map((e) => e.toLowerCase()));
21515
+ }
21516
+ async function loadGitignore(rootDir, readFile3) {
21517
+ let body;
21518
+ try {
21519
+ body = await readFile3(path2.join(rootDir, ".gitignore"));
21520
+ } catch {
21521
+ return () => false;
21522
+ }
21523
+ const regexes = [];
21524
+ for (const raw of body.split("\n")) {
21525
+ const line = raw.trim();
21526
+ if (line.length === 0 || line.startsWith("#")) continue;
21527
+ const anchored = line.startsWith("/");
21528
+ const pat = anchored ? line.slice(1) : line;
21529
+ const re = compileGlob2(pat.replace(/\/$/, ""));
21530
+ if (!re) continue;
21531
+ if (anchored) {
21532
+ regexes.push(new RegExp(`${re.source.slice(0, -1)}(?:/.*)?$`));
21533
+ } else {
21534
+ regexes.push(new RegExp(`(?:^|/)${re.source.slice(1, -1)}(?:/.*)?$`));
21535
+ }
21536
+ }
21537
+ if (regexes.length === 0) return () => false;
21538
+ return (relPath) => {
21539
+ const normalized = relPath.split(path2.sep).join("/");
21540
+ return regexes.some((re) => re.test(normalized));
21541
+ };
21542
+ }
21543
+ function looksBinary(text) {
21544
+ const limit = Math.min(text.length, 4096);
21545
+ for (let i = 0; i < limit; i++) {
21546
+ if (text.charCodeAt(i) === 0) return true;
20660
21547
  }
21548
+ return false;
21549
+ }
21550
+ var defaultGrepOperations = {
21551
+ isDirectory: (p) => statSync4(p).isDirectory(),
21552
+ readFile: (p) => readFileSync2(p, "utf-8"),
21553
+ readdir: (p) => readdirSync3(p)
20661
21554
  };
20662
21555
  var RegexFactory = class {
20663
21556
  static create(pattern, ignoreCase, literal) {
@@ -20678,12 +21571,22 @@ var RegexFactory = class {
20678
21571
  }
20679
21572
  };
20680
21573
  var SearchPlan = class {
20681
- constructor(searchPath, ops) {
21574
+ constructor(searchPath, ops, options) {
20682
21575
  this.searchPath = searchPath;
20683
21576
  this.ops = ops;
21577
+ this.include = options?.include ?? null;
21578
+ this.ignored = options?.ignored ?? null;
20684
21579
  }
20685
21580
  searchPath;
20686
21581
  ops;
21582
+ include;
21583
+ ignored;
21584
+ /**
21585
+ * Fully recursive (depth-first) enumeration of every readable file beneath
21586
+ * the search root. Pruned VCS/dependency folders and dot-directories are
21587
+ * skipped, directories are NEVER pushed as if they were files, and the
21588
+ * optional include/gitignore predicates scope or exclude the result set.
21589
+ */
20687
21590
  async collectFiles() {
20688
21591
  const filesToSearch = [];
20689
21592
  const isDir = await this.ops.isDirectory(this.searchPath);
@@ -20691,38 +21594,57 @@ var SearchPlan = class {
20691
21594
  filesToSearch.push(this.searchPath);
20692
21595
  return filesToSearch;
20693
21596
  }
20694
- const entries = await this.ops.readdir(this.searchPath);
21597
+ await this.walk(this.searchPath, filesToSearch);
21598
+ return filesToSearch;
21599
+ }
21600
+ relativeOf(fullPath) {
21601
+ return path2.relative(this.searchPath, fullPath);
21602
+ }
21603
+ async walk(dir, sink) {
21604
+ let entries;
21605
+ try {
21606
+ entries = await this.ops.readdir(dir);
21607
+ } catch {
21608
+ return;
21609
+ }
21610
+ entries.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
20695
21611
  for (const entry of entries) {
20696
- const fullPath = path2.join(this.searchPath, entry);
21612
+ const fullPath = path2.join(dir, entry);
21613
+ let entryIsDir;
20697
21614
  try {
20698
- if (await this.ops.isDirectory(fullPath) && !entry.startsWith(".")) {
20699
- const subEntries = await this.ops.readdir(fullPath);
20700
- for (const subEntry of subEntries) {
20701
- filesToSearch.push(path2.join(fullPath, subEntry));
20702
- }
20703
- } else if (!await this.ops.isDirectory(fullPath)) {
20704
- filesToSearch.push(fullPath);
20705
- }
21615
+ entryIsDir = await this.ops.isDirectory(fullPath);
20706
21616
  } catch {
21617
+ continue;
20707
21618
  }
21619
+ const rel = this.relativeOf(fullPath);
21620
+ if (entryIsDir) {
21621
+ if (PRUNED_DIRS.has(entry) || entry.startsWith(".")) continue;
21622
+ if (this.ignored && this.ignored(rel)) continue;
21623
+ await this.walk(fullPath, sink);
21624
+ continue;
21625
+ }
21626
+ if (this.ignored && this.ignored(rel)) continue;
21627
+ if (this.include && !this.include(rel)) continue;
21628
+ sink.push(fullPath);
20708
21629
  }
20709
- return filesToSearch;
20710
21630
  }
20711
21631
  };
20712
21632
  var MatchRenderer = class {
20713
- constructor(searchPath, contextValue) {
21633
+ constructor(searchPath, contextValue, before, after) {
20714
21634
  this.searchPath = searchPath;
20715
- this.contextValue = contextValue;
21635
+ this.before = before !== void 0 && before > 0 ? before : contextValue;
21636
+ this.after = after !== void 0 && after > 0 ? after : contextValue;
20716
21637
  }
20717
21638
  searchPath;
20718
- contextValue;
21639
+ before;
21640
+ after;
20719
21641
  renderMatch(filePath, lines, lineIndex) {
20720
21642
  const relativePath = path2.relative(this.searchPath, filePath);
20721
21643
  const line = lines[lineIndex] ?? "";
20722
21644
  const { wasTruncated } = truncateLine(line, GREP_MAX_LINE_LENGTH);
20723
21645
  const output = [];
20724
- const start = Math.max(0, lineIndex - this.contextValue);
20725
- const end = Math.min(lines.length - 1, lineIndex + this.contextValue);
21646
+ const start = Math.max(0, lineIndex - this.before);
21647
+ const end = Math.min(lines.length - 1, lineIndex + this.after);
20726
21648
  for (let j = start; j <= end; j++) {
20727
21649
  const isMatch = j === lineIndex;
20728
21650
  const prefix = isMatch ? ":" : "-";
@@ -20768,12 +21690,32 @@ var MatchRenderer = class {
20768
21690
  details: Object.keys(details).length > 0 ? details : void 0
20769
21691
  };
20770
21692
  }
21693
+ /** `files_with_matches` mode: one root-relative path per matching file. */
21694
+ formatFilesWithMatches(files) {
21695
+ if (files.length === 0) {
21696
+ return { output: "Pattern not found in any file" };
21697
+ }
21698
+ return { output: files.join("\n") };
21699
+ }
21700
+ /** `count` mode: `path:N` lines plus a roll-up of occurrences across files. */
21701
+ formatCount(counts) {
21702
+ if (counts.length === 0) {
21703
+ return { output: "Pattern not found in any file" };
21704
+ }
21705
+ const lines = counts.map((c) => `${c.file}:${c.count}`);
21706
+ const total = counts.reduce((sum, c) => sum + c.count, 0);
21707
+ const fileWord = counts.length === 1 ? "file" : "files";
21708
+ const occWord = total === 1 ? "occurrence" : "occurrences";
21709
+ lines.push("", `Found ${total} ${occWord} across ${counts.length} ${fileWord}`);
21710
+ return { output: lines.join("\n") };
21711
+ }
20771
21712
  };
20772
21713
  function createGrepTool(cwd, options) {
20773
21714
  const customOps = options?.operations;
20774
21715
  return {
20775
21716
  name: "grep",
20776
21717
  label: "grep",
21718
+ readOnly: true,
20777
21719
  description: `Search file contents for a pattern, returning every hit alongside its path and line number. The search halts at ${DEFAULT_LIMIT2} hits or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first. Any line over ${GREP_MAX_LINE_LENGTH} characters is shortened.`,
20778
21720
  parameters: grepSchema,
20779
21721
  execute: async (_toolCallId, {
@@ -20782,6 +21724,11 @@ function createGrepTool(cwd, options) {
20782
21724
  ignoreCase,
20783
21725
  literal,
20784
21726
  context,
21727
+ before,
21728
+ after,
21729
+ output_mode,
21730
+ glob,
21731
+ type,
20785
21732
  limit
20786
21733
  }, signal) => {
20787
21734
  return new Promise((resolve5, reject) => {
@@ -20797,7 +21744,10 @@ function createGrepTool(cwd, options) {
20797
21744
  const ops = customOps ?? defaultGrepOperations;
20798
21745
  const effectiveLimit = limit ?? DEFAULT_LIMIT2;
20799
21746
  const contextValue = context && context > 0 ? context : 0;
20800
- const searchPlan = new SearchPlan(searchPath, ops);
21747
+ const mode = output_mode ?? "content";
21748
+ const include = buildIncludePredicate(glob, type);
21749
+ const ignored = await loadGitignore(searchPath, (p) => ops.readFile(p));
21750
+ const searchPlan = new SearchPlan(searchPath, ops, { include, ignored });
20801
21751
  const filesToSearch = await searchPlan.collectFiles();
20802
21752
  if (filesToSearch.length === 0) {
20803
21753
  signal?.removeEventListener("abort", onAbort);
@@ -20812,34 +21762,63 @@ function createGrepTool(cwd, options) {
20812
21762
  reject(new Error(`Invalid regex pattern: ${e.message}`));
20813
21763
  return;
20814
21764
  }
20815
- const matchRenderer = new MatchRenderer(searchPath, contextValue);
21765
+ const matchRenderer = new MatchRenderer(searchPath, contextValue, before, after);
20816
21766
  const matches = [];
21767
+ const filesWithMatches = [];
21768
+ const counts = [];
20817
21769
  let linesTruncated = false;
21770
+ let stopScanning = false;
20818
21771
  for (const filePath of filesToSearch) {
20819
- if (matches.length >= effectiveLimit) {
21772
+ if (stopScanning) break;
21773
+ if (mode === "content" && matches.length >= effectiveLimit) {
20820
21774
  break;
20821
21775
  }
20822
21776
  try {
20823
21777
  const content = await ops.readFile(filePath);
21778
+ if (looksBinary(content)) {
21779
+ continue;
21780
+ }
20824
21781
  const lines = content.split("\n");
21782
+ const relativePath = path2.relative(searchPath, filePath);
21783
+ let fileCount = 0;
20825
21784
  for (let i = 0; i < lines.length; i++) {
20826
- if (matches.length >= effectiveLimit) {
21785
+ if (mode === "content" && matches.length >= effectiveLimit) {
21786
+ stopScanning = true;
20827
21787
  break;
20828
21788
  }
20829
21789
  const line = lines[i] ?? "";
20830
21790
  if (regex.test(line)) {
20831
- const rendered = matchRenderer.renderMatch(filePath, lines, i);
20832
- if (rendered.lineTruncated) {
20833
- linesTruncated = true;
21791
+ fileCount++;
21792
+ if (mode === "content") {
21793
+ const rendered = matchRenderer.renderMatch(filePath, lines, i);
21794
+ if (rendered.lineTruncated) {
21795
+ linesTruncated = true;
21796
+ }
21797
+ matches.push({ file: relativePath, line: i + 1, text: rendered.text });
21798
+ } else if (mode === "files_with_matches") {
21799
+ break;
20834
21800
  }
20835
- matches.push({ file: path2.relative(searchPath, filePath), line: i + 1, text: rendered.text });
21801
+ }
21802
+ }
21803
+ if (fileCount > 0) {
21804
+ if (mode === "files_with_matches") {
21805
+ filesWithMatches.push(relativePath);
21806
+ } else if (mode === "count") {
21807
+ counts.push({ file: relativePath, count: fileCount });
20836
21808
  }
20837
21809
  }
20838
21810
  } catch {
20839
21811
  }
20840
21812
  }
20841
21813
  signal?.removeEventListener("abort", onAbort);
20842
- const formatted = matchRenderer.formatOutput(matches, effectiveLimit, linesTruncated);
21814
+ let formatted;
21815
+ if (mode === "files_with_matches") {
21816
+ formatted = matchRenderer.formatFilesWithMatches(filesWithMatches);
21817
+ } else if (mode === "count") {
21818
+ formatted = matchRenderer.formatCount(counts);
21819
+ } else {
21820
+ formatted = matchRenderer.formatOutput(matches, effectiveLimit, linesTruncated);
21821
+ }
20843
21822
  resolve5({
20844
21823
  content: [{ type: "text", text: formatted.output }],
20845
21824
  details: formatted.details
@@ -20857,7 +21836,7 @@ var grepTool = createGrepTool(process.cwd());
20857
21836
 
20858
21837
  // src/facade/bot/actions/ls.ts
20859
21838
  import { Type as Type6 } from "@sinclair/typebox";
20860
- import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "fs";
21839
+ import { existsSync as existsSync4, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
20861
21840
  import nodePath from "path";
20862
21841
  var lsSchema = Type6.Object({
20863
21842
  path: Type6.Optional(Type6.String({ description: "Directory whose entries should be listed. Falls back to the working directory." })),
@@ -20866,8 +21845,8 @@ var lsSchema = Type6.Object({
20866
21845
  var DEFAULT_LIMIT3 = 512;
20867
21846
  var defaultLsOperations = {
20868
21847
  exists: existsSync4,
20869
- stat: statSync2,
20870
- readdir: readdirSync
21848
+ stat: statSync5,
21849
+ readdir: readdirSync4
20871
21850
  };
20872
21851
  function createAbortGuard(signal) {
20873
21852
  let aborted = signal?.aborted ?? false;
@@ -20952,8 +21931,8 @@ var DirectoryListRunner = class {
20952
21931
  if (!await this.ops.exists(resolvedDir)) {
20953
21932
  throw new Error(`No such path: ${resolvedDir}`);
20954
21933
  }
20955
- const stat2 = await this.ops.stat(resolvedDir);
20956
- if (!stat2.isDirectory()) {
21934
+ const stat3 = await this.ops.stat(resolvedDir);
21935
+ if (!stat3.isDirectory()) {
20957
21936
  throw new Error(`Target is not a directory: ${resolvedDir}`);
20958
21937
  }
20959
21938
  }
@@ -21009,6 +21988,7 @@ function createLsTool(cwd, options) {
21009
21988
  return {
21010
21989
  name: "ls",
21011
21990
  label: "ls",
21991
+ readOnly: true,
21012
21992
  description: `List the contents of a directory. Names are returned sorted alphabetically with dotfiles kept in, and subdirectories carry a trailing '/'. The listing halts at ${DEFAULT_LIMIT3} entries or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first.`,
21013
21993
  parameters: lsSchema,
21014
21994
  execute: async (_toolCallId, { path: path9, limit }, signal) => {
@@ -22435,24 +23415,25 @@ function withAbort2(signal, task) {
22435
23415
  });
22436
23416
  }
22437
23417
  var ReadExecutionPipeline = class {
22438
- constructor(cwd, ops, autoResizeImages) {
23418
+ constructor(cwd, ops, autoResizeImages, readState) {
22439
23419
  this.cwd = cwd;
22440
23420
  this.ops = ops;
22441
23421
  this.autoResizeImages = autoResizeImages;
23422
+ this.readState = readState;
22442
23423
  }
22443
23424
  cwd;
22444
23425
  ops;
22445
23426
  autoResizeImages;
23427
+ readState;
22446
23428
  async run(input, throwIfAborted2) {
22447
23429
  const absolutePath = resolveReadPath(input.path, this.cwd);
22448
23430
  await this.ops.access(absolutePath);
22449
23431
  throwIfAborted2();
22450
23432
  const mimeType = this.ops.detectImageMimeType ? await this.ops.detectImageMimeType(absolutePath) : void 0;
22451
23433
  throwIfAborted2();
22452
- if (mimeType) {
22453
- return this.readImageVariant(absolutePath, mimeType, throwIfAborted2);
22454
- }
22455
- return this.readTextVariant(absolutePath, input.path, input.offset, input.limit, throwIfAborted2);
23434
+ const result = mimeType ? await this.readImageVariant(absolutePath, mimeType, throwIfAborted2) : await this.readTextVariant(absolutePath, input.path, input.offset, input.limit, throwIfAborted2);
23435
+ recordReadState(this.readState, absolutePath);
23436
+ return result;
22456
23437
  }
22457
23438
  async readImageVariant(absolutePath, mimeType, throwIfAborted2) {
22458
23439
  const buffer = await this.ops.readFile(absolutePath);
@@ -22500,12 +23481,14 @@ ${dimensionNote}`;
22500
23481
  renderedText = `[Line ${startLineDisplay} weighs ${firstLineSize}, over the ${formatSize(DEFAULT_MAX_BYTES)} cap. Pull a byte slice via bash: sed -n '${startLineDisplay}p' ${requestedPath} | head -c ${DEFAULT_MAX_BYTES}]`;
22501
23482
  details = { truncation };
22502
23483
  } else if (truncation.truncated) {
22503
- renderedText = this.formatTruncationNotice(truncation, totalFileLines, startLineDisplay);
23484
+ const gutteredBody = this.addGutter(truncation.content, startLineDisplay);
23485
+ renderedText = this.formatTruncationNotice(gutteredBody, truncation, totalFileLines, startLineDisplay);
22504
23486
  details = { truncation };
22505
23487
  } else if (requestedLineCount !== void 0 && startLine + requestedLineCount < fileLines.length) {
22506
- renderedText = this.formatUserLimitNotice(truncation.content, fileLines.length, startLine, requestedLineCount);
23488
+ const gutteredBody = this.addGutter(truncation.content, startLineDisplay);
23489
+ renderedText = this.formatUserLimitNotice(gutteredBody, fileLines.length, startLine, requestedLineCount);
22507
23490
  } else {
22508
- renderedText = truncation.content;
23491
+ renderedText = this.addGutter(truncation.content, startLineDisplay);
22509
23492
  }
22510
23493
  throwIfAborted2();
22511
23494
  return {
@@ -22533,14 +23516,30 @@ ${dimensionNote}`;
22533
23516
  requestedLineCount: void 0
22534
23517
  };
22535
23518
  }
22536
- formatTruncationNotice(truncation, totalFileLines, startLineDisplay) {
23519
+ formatTruncationNotice(body, truncation, totalFileLines, startLineDisplay) {
22537
23520
  const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
22538
23521
  const nextOffset = endLineDisplay + 1;
22539
23522
  const continuationHint = this.buildContinuationHint(truncation, totalFileLines, startLineDisplay, endLineDisplay, nextOffset);
22540
- return `${truncation.content}
23523
+ return `${body}
22541
23524
 
22542
23525
  ${continuationHint}`;
22543
23526
  }
23527
+ /**
23528
+ * Prefix each line with a right-aligned 1-based absolute line number in the
23529
+ * cat -n style, giving the model stable edit/diff coordinates. `content` is
23530
+ * already windowed; `startLineDisplay` is the whole-file number of its first
23531
+ * line so the gutter stays correct under offset/limit paging. Empty content
23532
+ * (e.g. binary or an over-budget first line yielding "") passes through
23533
+ * unchanged so non-text output stays stable.
23534
+ */
23535
+ addGutter(content, startLineDisplay) {
23536
+ if (content.length === 0) return content;
23537
+ return content.split(/\r?\n/).map((line, i) => {
23538
+ const numStr = String(startLineDisplay + i);
23539
+ const label = numStr.length >= 6 ? numStr : numStr.padStart(6, " ");
23540
+ return `${label}\u2192${line}`;
23541
+ }).join("\n");
23542
+ }
22544
23543
  formatUserLimitNotice(baseText, totalLines, startLine, requestedLineCount) {
22545
23544
  const remaining = totalLines - (startLine + requestedLineCount);
22546
23545
  const nextOffset = startLine + requestedLineCount + 1;
@@ -22558,11 +23557,12 @@ ${continuationHint}`;
22558
23557
  function createReadTool(cwd, options) {
22559
23558
  const autoResizeImages = options?.autoResizeImages ?? true;
22560
23559
  const ops = options?.operations ?? defaultReadOperations;
22561
- const pipeline = new ReadExecutionPipeline(cwd, ops, autoResizeImages);
23560
+ const pipeline = new ReadExecutionPipeline(cwd, ops, autoResizeImages, options?.readState);
22562
23561
  return {
22563
23562
  name: "read",
22564
23563
  label: "read",
22565
- description: `Open a file and return its contents. Both text and images (jpg, png, gif, webp) work; images come back as attachments. Text output is capped at ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB, whichever comes first. For big files, page through with offset and limit, advancing offset until you have read everything you need.`,
23564
+ readOnly: true,
23565
+ description: `Open a file and return its contents. Both text and images (jpg, png, gif, webp) work; images come back as attachments. Output uses cat -n format - each line is prefixed with its 1-based line number. Text output is capped at ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB, whichever comes first. For big files, page through with offset and limit, advancing offset until you have read everything you need.`,
22566
23566
  parameters: readSchema,
22567
23567
  execute: async (_toolCallId, { path: path9, offset, limit }, signal) => {
22568
23568
  return withAbort2(signal, async ({ throwIfAborted: throwIfAborted2 }) => {
@@ -22579,9 +23579,9 @@ import { Type as Type15 } from "@sinclair/typebox";
22579
23579
  // src/facade/bot/actions/process-manager.ts
22580
23580
  import { spawn as spawn3 } from "node:child_process";
22581
23581
  import { EventEmitter } from "node:events";
22582
- import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync3 } from "node:fs";
22583
- import { tmpdir as tmpdir2 } from "node:os";
22584
- import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
23582
+ import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync3, rmSync, statSync as statSync6 } from "node:fs";
23583
+ import { tmpdir as tmpdir3 } from "node:os";
23584
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
22585
23585
 
22586
23586
  // src/facade/bot/actions/process-types.ts
22587
23587
  var MESSAGE_TYPE_PROCESS_UPDATE = "ad-process:update";
@@ -22646,7 +23646,7 @@ var ProcessManager = class {
22646
23646
  watcher = null;
22647
23647
  getConfiguredShellPath;
22648
23648
  constructor(options) {
22649
- this.logDir = join2(tmpdir2(), `indusagi-processes-${Date.now()}`);
23649
+ this.logDir = join3(tmpdir3(), `indusagi-processes-${Date.now()}`);
22650
23650
  mkdirSync(this.logDir, { recursive: true });
22651
23651
  this.getConfiguredShellPath = options?.getConfiguredShellPath ?? (() => void 0);
22652
23652
  }
@@ -22715,9 +23715,9 @@ var ProcessManager = class {
22715
23715
  }
22716
23716
  start(name, command, cwd, options) {
22717
23717
  const id = `proc_${++this.counter}`;
22718
- const stdoutFile = join2(this.logDir, `${id}-stdout.log`);
22719
- const stderrFile = join2(this.logDir, `${id}-stderr.log`);
22720
- const combinedFile = join2(this.logDir, `${id}-combined.log`);
23718
+ const stdoutFile = join3(this.logDir, `${id}-stdout.log`);
23719
+ const stderrFile = join3(this.logDir, `${id}-stderr.log`);
23720
+ const combinedFile = join3(this.logDir, `${id}-combined.log`);
22721
23721
  appendFileSync(stdoutFile, "");
22722
23722
  appendFileSync(stderrFile, "");
22723
23723
  appendFileSync(combinedFile, "");
@@ -22867,8 +23867,8 @@ var ProcessManager = class {
22867
23867
  }
22868
23868
  try {
22869
23869
  return {
22870
- stdout: readFileSync2(managed.stdoutFile, "utf-8"),
22871
- stderr: readFileSync2(managed.stderrFile, "utf-8")
23870
+ stdout: readFileSync3(managed.stdoutFile, "utf-8"),
23871
+ stderr: readFileSync3(managed.stderrFile, "utf-8")
22872
23872
  };
22873
23873
  } catch {
22874
23874
  return { stdout: "", stderr: "" };
@@ -23024,8 +24024,8 @@ var ProcessManager = class {
23024
24024
  }
23025
24025
  try {
23026
24026
  return {
23027
- stdout: statSync3(managed.stdoutFile).size,
23028
- stderr: statSync3(managed.stderrFile).size
24027
+ stdout: statSync6(managed.stdoutFile).size,
24028
+ stderr: statSync6(managed.stderrFile).size
23029
24029
  };
23030
24030
  } catch {
23031
24031
  return { stdout: 0, stderr: 0 };
@@ -23033,7 +24033,7 @@ var ProcessManager = class {
23033
24033
  }
23034
24034
  readTailLines(filePath, lines) {
23035
24035
  try {
23036
- const content = readFileSync2(filePath, "utf-8");
24036
+ const content = readFileSync3(filePath, "utf-8");
23037
24037
  const allLines = content.split("\n");
23038
24038
  if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
23039
24039
  allLines.pop();
@@ -23596,6 +24596,7 @@ function createTodoReadTool(store) {
23596
24596
  return {
23597
24597
  name: "todoread",
23598
24598
  label: "todoread",
24599
+ readOnly: true,
23599
24600
  description: "Read the current todo list. Returns items with content, status, and priority.",
23600
24601
  parameters: TodoReadSchema,
23601
24602
  execute: async () => {
@@ -23736,6 +24737,7 @@ function createWebFetchTool(options) {
23736
24737
  return {
23737
24738
  name: "webfetch",
23738
24739
  label: "webfetch",
24740
+ readOnly: true,
23739
24741
  description: "Fetches content from a specified URL. Takes a URL and optional format as input. Fetches URL content, converts to requested format (markdown by default). Returns content in the specified format. Use this tool when you need to retrieve and analyze web content. The URL must be a fully-formed valid URL starting with http:// or https://. Format options: 'markdown' (default), 'text', or 'html'. This tool is read-only and does not modify any files.",
23740
24742
  parameters: webFetchSchema,
23741
24743
  execute: async (_toolCallId, params, signal) => {
@@ -23832,6 +24834,62 @@ var webFetchTool = createWebFetchTool();
23832
24834
 
23833
24835
  // src/facade/bot/actions/websearch.ts
23834
24836
  import { Type as Type18 } from "@sinclair/typebox";
24837
+ var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/";
24838
+ var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
24839
+ function toPlainText(html) {
24840
+ const withoutTags = html.replace(/<[^>]*>/g, "");
24841
+ const decoded = withoutTags.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ");
24842
+ return decoded.replace(/\s+/g, " ").trim();
24843
+ }
24844
+ function unwrapTarget(href) {
24845
+ let candidate = href;
24846
+ const redirectMatch = /[?&]uddg=([^&]+)/.exec(candidate);
24847
+ if (redirectMatch) {
24848
+ try {
24849
+ candidate = decodeURIComponent(redirectMatch[1]);
24850
+ } catch {
24851
+ }
24852
+ }
24853
+ if (candidate.startsWith("//")) {
24854
+ candidate = `https:${candidate}`;
24855
+ }
24856
+ return candidate;
24857
+ }
24858
+ function extractHits(body, limit) {
24859
+ const hits = [];
24860
+ const seen = /* @__PURE__ */ new Set();
24861
+ const titleAnchor = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
24862
+ const snippetBlock = /\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/[a-z]+>/i;
24863
+ let match;
24864
+ while ((match = titleAnchor.exec(body)) !== null) {
24865
+ if (hits.length >= limit) break;
24866
+ const url = unwrapTarget(match[1]);
24867
+ const title = toPlainText(match[2]);
24868
+ if (url.length === 0 || title.length === 0) continue;
24869
+ if (seen.has(url)) continue;
24870
+ const tail = body.slice(titleAnchor.lastIndex);
24871
+ const snippetMatch = snippetBlock.exec(tail);
24872
+ const snippet = snippetMatch ? toPlainText(snippetMatch[1]) : "";
24873
+ seen.add(url);
24874
+ hits.push({ title, url, snippet });
24875
+ }
24876
+ return hits;
24877
+ }
24878
+ function renderHits(query, hits) {
24879
+ const lines = [
24880
+ `${hits.length} result${hits.length === 1 ? "" : "s"} for "${query}":`,
24881
+ ""
24882
+ ];
24883
+ hits.forEach((hit, index) => {
24884
+ lines.push(`${index + 1}. ${hit.title}`);
24885
+ lines.push(` ${hit.url}`);
24886
+ if (hit.snippet.length > 0) {
24887
+ lines.push(` ${hit.snippet}`);
24888
+ }
24889
+ lines.push("");
24890
+ });
24891
+ return lines.join("\n").trimEnd();
24892
+ }
23835
24893
  var webSearchSchema = Type18.Object({
23836
24894
  query: Type18.String({ description: "Web search query" }),
23837
24895
  numResults: Type18.Optional(
@@ -23843,48 +24901,31 @@ var requestCount = 0;
23843
24901
  async function defaultWebSearch(query, numResults, signal) {
23844
24902
  const count = Math.min(numResults ?? 8, 10);
23845
24903
  try {
23846
- const searchUrl = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
23847
- const response = await fetch(searchUrl, {
24904
+ const form = new URLSearchParams({ q: query });
24905
+ const response = await fetch(SEARCH_ENDPOINT, {
24906
+ method: "POST",
23848
24907
  signal,
23849
24908
  headers: {
23850
- "Accept": "application/json"
23851
- }
24909
+ "Content-Type": "application/x-www-form-urlencoded",
24910
+ "User-Agent": USER_AGENT,
24911
+ Accept: "text/html"
24912
+ },
24913
+ body: form.toString()
23852
24914
  });
23853
24915
  if (!response.ok) {
23854
24916
  throw new Error(`Search request failed: ${response.status}`);
23855
24917
  }
23856
- const data = await response.json();
23857
- const results = [];
23858
- if (data.AbstractText) {
23859
- results.push(`**Summary:** ${data.AbstractText}`);
23860
- if (data.AbstractURL) {
23861
- results.push(`Source: ${data.AbstractURL}`);
23862
- }
23863
- }
23864
- if (data.RelatedTopics && Array.isArray(data.RelatedTopics)) {
23865
- const topics = data.RelatedTopics.filter((t) => t.Text && t.FirstURL).slice(0, count);
23866
- if (topics.length > 0) {
23867
- results.push(`
23868
- **Related Results:**`);
23869
- for (const topic of topics) {
23870
- results.push(`- ${topic.Text}`);
23871
- results.push(` URL: ${topic.FirstURL}`);
23872
- }
23873
- }
23874
- }
23875
- if (results.length === 0) {
24918
+ const body = await response.text();
24919
+ const hits = extractHits(body, count);
24920
+ if (hits.length === 0) {
23876
24921
  return `No results found for: "${query}". Try a different search query.`;
23877
24922
  }
23878
- return `Web search results for: "${query}"
23879
-
23880
- ${results.join("\n")}`;
24923
+ return renderHits(query, hits);
23881
24924
  } catch (error) {
23882
- if (error.name === "AbortError") {
24925
+ if (error?.name === "AbortError") {
23883
24926
  throw new Error("Web search request timed out or was aborted");
23884
24927
  }
23885
- return `Search query: "${query}"
23886
-
23887
- Unable to perform live search. Please try again or use a different query.`;
24928
+ throw error;
23888
24929
  }
23889
24930
  }
23890
24931
  function createWebSearchTool(options) {
@@ -23892,6 +24933,7 @@ function createWebSearchTool(options) {
23892
24933
  return {
23893
24934
  name: "websearch",
23894
24935
  label: "websearch",
24936
+ readOnly: true,
23895
24937
  description: "Search the web - performs real-time web searches and can scrape content from specific URLs. Provides up-to-date information for current events and recent data. Use this tool for accessing information beyond knowledge cutoff. The current year is 2026. You MUST use this year when searching for recent information or current events (e.g., search for 'AI news 2026', NOT 'AI news 2025').",
23896
24938
  parameters: webSearchSchema,
23897
24939
  execute: async (_toolCallId, { query, numResults }, signal) => {
@@ -23950,8 +24992,9 @@ var webSearchTool = createWebSearchTool();
23950
24992
 
23951
24993
  // src/facade/bot/actions/write.ts
23952
24994
  import { Type as Type19 } from "@sinclair/typebox";
24995
+ import { existsSync as existsSync6 } from "fs";
23953
24996
  import { copyFile as fsCopyFile, mkdir as fsMkdir, writeFile as fsWriteFile2 } from "fs/promises";
23954
- import { dirname } from "path";
24997
+ import { dirname as dirname2 } from "path";
23955
24998
  var writeSchema = Type19.Object({
23956
24999
  path: Type19.String({ description: "Destination file, given as a relative or absolute path" }),
23957
25000
  content: Type19.String({ description: "The text that will become the file's contents" })
@@ -23970,22 +25013,48 @@ var FileValidator = class {
23970
25013
  }
23971
25014
  };
23972
25015
  var WriteSession = class {
23973
- constructor(cwd, relativePath, fileContent, operations, createBackup) {
25016
+ constructor(cwd, relativePath, fileContent, operations, createBackup, readState, checkpoint) {
23974
25017
  this.cwd = cwd;
23975
25018
  this.relativePath = relativePath;
23976
25019
  this.fileContent = fileContent;
23977
25020
  this.operations = operations;
23978
25021
  this.createBackup = createBackup;
25022
+ this.readState = readState;
25023
+ this.checkpoint = checkpoint;
23979
25024
  this.absolutePath = resolveToCwd(relativePath, cwd);
23980
- this.targetDir = dirname(this.absolutePath);
25025
+ this.targetDir = dirname2(this.absolutePath);
23981
25026
  }
23982
25027
  cwd;
23983
25028
  relativePath;
23984
25029
  fileContent;
23985
25030
  operations;
23986
25031
  createBackup;
25032
+ readState;
25033
+ checkpoint;
23987
25034
  absolutePath;
23988
25035
  targetDir;
25036
+ /**
25037
+ * Enforce the read-before-edit + staleness gate ahead of an OVERWRITE.
25038
+ * Brand-new files (those not yet on disk) are exempt — there is nothing the
25039
+ * model could have clobbered. No-op without a handle.
25040
+ */
25041
+ enforceGate() {
25042
+ if (!this.readState) return;
25043
+ if (!existsSync6(this.absolutePath)) return;
25044
+ const gate = enforceReadGate(this.readState, this.absolutePath);
25045
+ if (!gate.ok) {
25046
+ throw new Error(gate.message);
25047
+ }
25048
+ }
25049
+ /**
25050
+ * Snapshot the file's pre-mutation on-disk content for rewind. Runs AFTER the
25051
+ * read-state gate and BEFORE any mutation (mkdir/write), so the captured bytes
25052
+ * are the OLD content; a brand-new file is recorded as `null`. No-op without a
25053
+ * handle.
25054
+ */
25055
+ recordCheckpoint() {
25056
+ recordCheckpoint(this.checkpoint, this.absolutePath);
25057
+ }
23989
25058
  async prepareDirectories() {
23990
25059
  await this.operations.mkdir(this.targetDir);
23991
25060
  }
@@ -24000,6 +25069,7 @@ var WriteSession = class {
24000
25069
  }
24001
25070
  async persistContent() {
24002
25071
  await this.operations.writeFile(this.absolutePath, this.fileContent);
25072
+ recordReadState(this.readState, this.absolutePath);
24003
25073
  }
24004
25074
  buildResponse() {
24005
25075
  return {
@@ -24064,8 +25134,20 @@ function createWriteTool(cwd, options) {
24064
25134
  parameters: writeSchema,
24065
25135
  execute: async (_toolCallId, { path: path9, content }, signal) => {
24066
25136
  validator.validate(path9);
24067
- const writeSession = new WriteSession(cwd, path9, content, operations, Boolean(options?.createBackup));
25137
+ const writeSession = new WriteSession(
25138
+ cwd,
25139
+ path9,
25140
+ content,
25141
+ operations,
25142
+ Boolean(options?.createBackup),
25143
+ options?.readState,
25144
+ options?.checkpoint
25145
+ );
24068
25146
  return withAbort3(signal, async (isAborted) => {
25147
+ writeSession.enforceGate();
25148
+ throwIfAborted(isAborted);
25149
+ writeSession.recordCheckpoint();
25150
+ throwIfAborted(isAborted);
24069
25151
  await writeSession.prepareDirectories();
24070
25152
  throwIfAborted(isAborted);
24071
25153
  await writeSession.backupIfEnabled();
@@ -24689,13 +25771,13 @@ async function gcStaleTeamDirs(opts) {
24689
25771
  continue;
24690
25772
  }
24691
25773
  const teamDir = path4.join(teamsRootAbs, teamId);
24692
- let stat2;
25774
+ let stat3;
24693
25775
  try {
24694
- stat2 = await fs2.promises.stat(teamDir);
25776
+ stat3 = await fs2.promises.stat(teamDir);
24695
25777
  } catch {
24696
25778
  continue;
24697
25779
  }
24698
- if (!stat2.isDirectory()) continue;
25780
+ if (!stat3.isDirectory()) continue;
24699
25781
  let ageMs;
24700
25782
  try {
24701
25783
  const configPath = path4.join(teamDir, "config.json");
@@ -24704,12 +25786,12 @@ async function gcStaleTeamDirs(opts) {
24704
25786
  const createdAt = typeof config === "object" && config !== null && "createdAt" in config ? config.createdAt : void 0;
24705
25787
  if (typeof createdAt === "string") {
24706
25788
  const ts = Date.parse(createdAt);
24707
- ageMs = Number.isFinite(ts) ? now - ts : now - stat2.mtimeMs;
25789
+ ageMs = Number.isFinite(ts) ? now - ts : now - stat3.mtimeMs;
24708
25790
  } else {
24709
- ageMs = now - stat2.mtimeMs;
25791
+ ageMs = now - stat3.mtimeMs;
24710
25792
  }
24711
25793
  } catch {
24712
- ageMs = now - stat2.mtimeMs;
25794
+ ageMs = now - stat3.mtimeMs;
24713
25795
  }
24714
25796
  if (ageMs < maxAgeMs) {
24715
25797
  skipped.push({ teamId, reason: "too recent" });
@@ -26352,17 +27434,17 @@ import { homedir as homedir2 } from "os";
26352
27434
  import {
26353
27435
  appendFileSync as appendFileSync2,
26354
27436
  closeSync as closeSync2,
26355
- existsSync as existsSync7,
27437
+ existsSync as existsSync8,
26356
27438
  mkdirSync as mkdirSync2,
26357
27439
  openSync as openSync2,
26358
- readdirSync as readdirSync2,
26359
- readFileSync as readFileSync3,
27440
+ readdirSync as readdirSync5,
27441
+ readFileSync as readFileSync4,
26360
27442
  readSync,
26361
- statSync as statSync5,
27443
+ statSync as statSync8,
26362
27444
  writeFileSync as writeFileSync2
26363
27445
  } from "fs";
26364
- import { readdir, readFile as readFile2, stat } from "fs/promises";
26365
- import { join as join9, resolve as resolve4 } from "path";
27446
+ import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
27447
+ import { join as join10, resolve as resolve4 } from "path";
26366
27448
  var CURRENT_SESSION_VERSION = 3;
26367
27449
  function generateId(byId) {
26368
27450
  let attempt = 0;
@@ -26542,11 +27624,11 @@ function getDefaultAgentDir() {
26542
27624
  return expandHomePath(explicitAgentDir);
26543
27625
  }
26544
27626
  const rawBase = process.env[ENV_BASE_DIR];
26545
- const baseDir = rawBase ? expandHomePath(rawBase) : join9(homedir2(), DEFAULT_CONFIG_DIR);
26546
- return join9(baseDir, "agent");
27627
+ const baseDir = rawBase ? expandHomePath(rawBase) : join10(homedir2(), DEFAULT_CONFIG_DIR);
27628
+ return join10(baseDir, "agent");
26547
27629
  }
26548
27630
  function getSessionsDir() {
26549
- return join9(getDefaultAgentDir(), "sessions");
27631
+ return join10(getDefaultAgentDir(), "sessions");
26550
27632
  }
26551
27633
  function encodeCwdForDir(cwd) {
26552
27634
  const trimmedLeadingSlash = cwd.replace(/^[/\\]/, "");
@@ -26554,8 +27636,8 @@ function encodeCwdForDir(cwd) {
26554
27636
  return `--${sanitized}--`;
26555
27637
  }
26556
27638
  function getDefaultSessionDir(cwd) {
26557
- const sessionDir = join9(getDefaultAgentDir(), "sessions", encodeCwdForDir(cwd));
26558
- if (!existsSync7(sessionDir)) {
27639
+ const sessionDir = join10(getDefaultAgentDir(), "sessions", encodeCwdForDir(cwd));
27640
+ if (!existsSync8(sessionDir)) {
26559
27641
  mkdirSync2(sessionDir, { recursive: true });
26560
27642
  }
26561
27643
  return sessionDir;
@@ -26565,10 +27647,10 @@ function hasValidSessionHeader(entries) {
26565
27647
  return first !== void 0 && first.type === "session" && typeof first.id === "string";
26566
27648
  }
26567
27649
  function loadEntriesFromFile(filePath) {
26568
- if (!existsSync7(filePath)) {
27650
+ if (!existsSync8(filePath)) {
26569
27651
  return [];
26570
27652
  }
26571
- const entries = decodeJsonlLines(readFileSync3(filePath, "utf8"));
27653
+ const entries = decodeJsonlLines(readFileSync4(filePath, "utf8"));
26572
27654
  if (entries.length === 0) {
26573
27655
  return entries;
26574
27656
  }
@@ -26596,7 +27678,7 @@ function isValidSessionFile(filePath) {
26596
27678
  }
26597
27679
  function findMostRecentSession(sessionDir) {
26598
27680
  try {
26599
- const ranked = readdirSync2(sessionDir).filter((f) => f.endsWith(".jsonl")).map((f) => join9(sessionDir, f)).filter(isValidSessionFile).map((path9) => ({ path: path9, mtime: statSync5(path9).mtime })).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
27681
+ const ranked = readdirSync5(sessionDir).filter((f) => f.endsWith(".jsonl")).map((f) => join10(sessionDir, f)).filter(isValidSessionFile).map((path9) => ({ path: path9, mtime: statSync8(path9).mtime })).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
26600
27682
  return ranked[0]?.path || null;
26601
27683
  } catch {
26602
27684
  return null;
@@ -26665,7 +27747,7 @@ async function buildSessionInfo(filePath) {
26665
27747
  return null;
26666
27748
  }
26667
27749
  const sessionHeader = header;
26668
- const stats = await stat(filePath);
27750
+ const stats = await stat2(filePath);
26669
27751
  let messageCount = 0;
26670
27752
  let firstMessage = "";
26671
27753
  let name;
@@ -26710,11 +27792,11 @@ async function buildSessionInfo(filePath) {
26710
27792
  }
26711
27793
  }
26712
27794
  async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progressTotal) {
26713
- if (!existsSync7(dir)) {
27795
+ if (!existsSync8(dir)) {
26714
27796
  return [];
26715
27797
  }
26716
27798
  try {
26717
- const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join9(dir, f));
27799
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join10(dir, f));
26718
27800
  const total = progressTotal ?? files.length;
26719
27801
  let loaded = 0;
26720
27802
  const results = await Promise.all(
@@ -26745,7 +27827,7 @@ var SessionManager = class _SessionManager {
26745
27827
  this.cwd = cwd;
26746
27828
  this.sessionDir = sessionDir;
26747
27829
  this.persist = persist;
26748
- if (persist && sessionDir && !existsSync7(sessionDir)) {
27830
+ if (persist && sessionDir && !existsSync8(sessionDir)) {
26749
27831
  mkdirSync2(sessionDir, { recursive: true });
26750
27832
  }
26751
27833
  if (sessionFile) {
@@ -26758,7 +27840,7 @@ var SessionManager = class _SessionManager {
26758
27840
  setSessionFile(sessionFile) {
26759
27841
  const resolved = resolve4(sessionFile);
26760
27842
  this.sessionFile = resolved;
26761
- if (!existsSync7(resolved)) {
27843
+ if (!existsSync8(resolved)) {
26762
27844
  this.newSession();
26763
27845
  this.sessionFile = resolved;
26764
27846
  return;
@@ -26797,7 +27879,7 @@ var SessionManager = class _SessionManager {
26797
27879
  this.flushed = false;
26798
27880
  if (this.persist) {
26799
27881
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
26800
- this.sessionFile = join9(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
27882
+ this.sessionFile = join10(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
26801
27883
  }
26802
27884
  return this.sessionFile;
26803
27885
  }
@@ -27153,7 +28235,7 @@ var SessionManager = class _SessionManager {
27153
28235
  const newSessionId = randomUUID();
27154
28236
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
27155
28237
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
27156
- const newSessionFile = join9(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
28238
+ const newSessionFile = join10(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
27157
28239
  const header = {
27158
28240
  type: "session",
27159
28241
  version: CURRENT_SESSION_VERSION,
@@ -27270,13 +28352,13 @@ var SessionManager = class _SessionManager {
27270
28352
  throw new Error(`Fork aborted \u2014 source session ${sourcePath} is missing its header record`);
27271
28353
  }
27272
28354
  const dir = sessionDir ?? getDefaultSessionDir(targetCwd);
27273
- if (!existsSync7(dir)) {
28355
+ if (!existsSync8(dir)) {
27274
28356
  mkdirSync2(dir, { recursive: true });
27275
28357
  }
27276
28358
  const newSessionId = randomUUID();
27277
28359
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
27278
28360
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
27279
- const newSessionFile = join9(dir, `${fileTimestamp}_${newSessionId}.jsonl`);
28361
+ const newSessionFile = join10(dir, `${fileTimestamp}_${newSessionId}.jsonl`);
27280
28362
  const newHeader = {
27281
28363
  type: "session",
27282
28364
  version: CURRENT_SESSION_VERSION,
@@ -27314,16 +28396,16 @@ var SessionManager = class _SessionManager {
27314
28396
  static async listAll(onProgress) {
27315
28397
  const sessionsDir = getSessionsDir();
27316
28398
  try {
27317
- if (!existsSync7(sessionsDir)) {
28399
+ if (!existsSync8(sessionsDir)) {
27318
28400
  return [];
27319
28401
  }
27320
28402
  const rootEntries = await readdir(sessionsDir, { withFileTypes: true });
27321
- const dirs = rootEntries.filter((e) => e.isDirectory()).map((e) => join9(sessionsDir, e.name));
28403
+ const dirs = rootEntries.filter((e) => e.isDirectory()).map((e) => join10(sessionsDir, e.name));
27322
28404
  const perDirFiles = [];
27323
28405
  let totalFiles = 0;
27324
28406
  for (const dir of dirs) {
27325
28407
  try {
27326
- const jsonlFiles = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join9(dir, f));
28408
+ const jsonlFiles = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join10(dir, f));
27327
28409
  perDirFiles.push(jsonlFiles);
27328
28410
  totalFiles += jsonlFiles.length;
27329
28411
  } catch {
@@ -27361,11 +28443,14 @@ export {
27361
28443
  ComposioService,
27362
28444
  DEFAULT_MAX_BYTES,
27363
28445
  DEFAULT_MAX_LINES,
28446
+ DESANITIZATIONS,
28447
+ FILE_NOT_FOUND_CWD_NOTE,
27364
28448
  IndusagiComposioProvider,
27365
28449
  LIVE_STATUSES,
27366
28450
  MESSAGE_TYPE_PROCESS_UPDATE,
27367
28451
  PIRATE_NAME_POOL,
27368
28452
  ProcessManager,
28453
+ READ_ONLY_TOOL_NAMES,
27369
28454
  SessionManager,
27370
28455
  TEAM_ATTACH_CLAIM_FILE,
27371
28456
  TEAM_ATTACH_CLAIM_STALE_MS,
@@ -27384,10 +28469,15 @@ export {
27384
28469
  agentLoop,
27385
28470
  agentLoopContinue,
27386
28471
  allTools,
28472
+ argvToCommandString,
27387
28473
  assertTeamDirWithinTeamsRoot,
27388
28474
  assessAttachClaimFreshness,
27389
28475
  bashExecutionToText,
27390
28476
  bashTool,
28477
+ buildBwrapArgv,
28478
+ buildSandboxArgv,
28479
+ buildSeatbeltArgv,
28480
+ buildSeatbeltProfile,
27391
28481
  buildSessionContext,
27392
28482
  claimNextAvailableTask,
27393
28483
  claimTask,
@@ -27424,6 +28514,7 @@ export {
27424
28514
  createProcessTool,
27425
28515
  createReadOnlyTools,
27426
28516
  createReadTool,
28517
+ createSandboxedBashOperations,
27427
28518
  createTask,
27428
28519
  createTodoReadTool,
27429
28520
  createTodoWriteTool,
@@ -27431,11 +28522,13 @@ export {
27431
28522
  createWebFetchTool,
27432
28523
  createWebSearchTool,
27433
28524
  createWriteTool,
28525
+ desanitizeMatchString,
27434
28526
  editTool,
27435
28527
  ensureTeamConfig,
27436
28528
  ensureWorktreeCwd,
27437
28529
  expandPath,
27438
28530
  findMostRecentSession,
28531
+ findSimilarFile,
27439
28532
  findTool,
27440
28533
  formatProviderModel,
27441
28534
  formatSize,
@@ -27477,21 +28570,25 @@ export {
27477
28570
  pickNamesFromPool,
27478
28571
  pickPirateNames,
27479
28572
  popUnreadMessages,
28573
+ preserveQuoteStyle,
27480
28574
  processTool,
27481
28575
  readOnlyTools,
27482
28576
  readTeamAttachClaim,
27483
28577
  readTool,
27484
28578
  releaseTeamAttachClaim,
27485
28579
  removeTaskDependency,
28580
+ replaceAllLiteral,
27486
28581
  resolveReadPath,
27487
28582
  resolveTeammateModelSelection,
27488
28583
  resolveToCwd,
28584
+ sandboxAvailability,
27489
28585
  sanitizeName,
27490
28586
  setMemberStatus,
27491
28587
  setTeamStyle,
27492
28588
  shortTaskId,
27493
28589
  startAssignedTask,
27494
28590
  streamProxy,
28591
+ suggestPathUnderCwd,
27495
28592
  taskAssignmentPayload,
27496
28593
  todoReadTool,
27497
28594
  todoWriteTool,