// src/capabilities/kernel/spec.ts
var READ_STATE_HANDLE_KEY = "readState";
function coerceInput(raw) {
  if (typeof raw === "string") {
    const trimmed = raw.trim();
    if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
      try {
        return JSON.parse(trimmed);
      } catch {
      }
    }
    return raw;
  }
  if (raw === null || raw === void 0) {
    return {};
  }
  return raw;
}
function describeThrow(err) {
  if (err instanceof Error) return err.message;
  if (typeof err === "string") return err;
  try {
    return JSON.stringify(err);
  } catch {
    return String(err);
  }
}
function projectOutcome(id, result) {
  const blocks = result.content;
  let output;
  if (blocks.length === 1) {
    const only = blocks[0];
    output = only.kind === "text" ? only.text : only.value;
  } else {
    output = blocks;
  }
  return { id, output, isError: result.isError === true };
}
function defineTool(spec) {
  return {
    name: spec.name,
    // Facade `AgentTool` surface: top-level schema fields + an `execute` adapter
    // so the tool is usable by the high-level `Agent` as well as the kernel.
    label: spec.name,
    description: spec.description,
    parameters: spec.parameters,
    async execute(_toolCallId, params, signal) {
      const ctx = {
        cwd: process.cwd(),
        signal: signal ?? new AbortController().signal
      };
      try {
        const result = await spec.run(coerceInput(params), ctx);
        return {
          content: result.content.map(
            (block) => block.kind === "text" ? { type: "text", text: block.text } : { type: "text", text: JSON.stringify(block.value) }
          ),
          details: {},
          isError: result.isError ?? false
        };
      } catch (err) {
        return {
          content: [{ type: "text", text: `${spec.name} did not finish: ${describeThrow(err)}` }],
          details: {},
          isError: true
        };
      }
    },
    descriptor() {
      return {
        name: spec.name,
        description: spec.description,
        parameters: spec.parameters
      };
    },
    async invoke(call, ctx) {
      if (ctx.signal.aborted) {
        return {
          id: call.id,
          output: `Cancelled before ${spec.name} could begin.`,
          isError: true
        };
      }
      try {
        const input = coerceInput(call.input);
        const result = await spec.run(input, ctx);
        return projectOutcome(call.id, result);
      } catch (err) {
        return {
          id: call.id,
          output: `${spec.name} did not finish: ${describeThrow(err)}`,
          isError: true
        };
      }
    }
  };
}

// src/capabilities/kernel/output.ts
function defaultNotice(omitted) {
  return `
\u2026 trimmed ${omitted} byte${omitted === 1 ? "" : "s"} to fit the window \u2026
`;
}
function boundaryFromStart(text, byteTarget) {
  let bytes = 0;
  let i = 0;
  while (i < text.length) {
    const cp = text.codePointAt(i);
    const width = utf8Width(cp);
    if (bytes + width > byteTarget) break;
    bytes += width;
    i += cp > 65535 ? 2 : 1;
  }
  return i;
}
function boundaryFromEnd(text, byteTarget) {
  let bytes = 0;
  let i = text.length;
  while (i > 0) {
    let prev = i - 1;
    if (prev > 0 && isLowSurrogate(text.charCodeAt(prev)) && isHighSurrogate(text.charCodeAt(prev - 1))) {
      prev -= 1;
    }
    const cp = text.codePointAt(prev);
    const width = utf8Width(cp);
    if (bytes + width > byteTarget) break;
    bytes += width;
    i = prev;
  }
  return i;
}
function utf8Width(cp) {
  if (cp <= 127) return 1;
  if (cp <= 2047) return 2;
  if (cp <= 65535) return 3;
  return 4;
}
function isHighSurrogate(unit) {
  return unit >= 55296 && unit <= 56319;
}
function isLowSurrogate(unit) {
  return unit >= 56320 && unit <= 57343;
}
function clamp(text, policy) {
  const total = Buffer.byteLength(text, "utf8");
  if (total <= policy.maxBytes) return text;
  const mark = policy.notice ?? defaultNotice;
  if (policy.kind === "head") {
    const cut = boundaryFromStart(text, policy.maxBytes);
    const kept = text.slice(0, cut);
    const omitted2 = total - Buffer.byteLength(kept, "utf8");
    return kept + mark(omitted2);
  }
  if (policy.kind === "tail") {
    const cut = boundaryFromEnd(text, policy.maxBytes);
    const kept = text.slice(cut);
    const omitted2 = total - Buffer.byteLength(kept, "utf8");
    return mark(omitted2) + kept;
  }
  const headShare = Math.floor(policy.maxBytes / 2);
  const tailShare = policy.maxBytes - headShare;
  const headCut = boundaryFromStart(text, headShare);
  const tailCut = boundaryFromEnd(text, tailShare);
  if (tailCut <= headCut) return text;
  const headKept = text.slice(0, headCut);
  const tailKept = text.slice(tailCut);
  const omitted = total - Buffer.byteLength(headKept, "utf8") - Buffer.byteLength(tailKept, "utf8");
  return headKept + mark(omitted) + tailKept;
}

// src/capabilities/kernel/registry.ts
var UnknownToolError = class extends Error {
  /** The offending tool or collection name. */
  missing;
  constructor(kind, missing) {
    super(`No ${kind} named "${missing}" is registered.`);
    this.name = "UnknownToolError";
    this.missing = missing;
  }
};
var ToolRegistry = class {
  tools = /* @__PURE__ */ new Map();
  collections = /* @__PURE__ */ new Map();
  /** Add a defined tool. A later registration under the same name replaces it. */
  register(tool) {
    this.tools.set(tool.name, tool);
    return this;
  }
  /** Register several tools in one call. */
  registerAll(tools) {
    for (const tool of tools) this.register(tool);
    return this;
  }
  /** True when a tool with this name has been registered. */
  has(name) {
    return this.tools.has(name);
  }
  /** Fetch a registered tool, or throw {@link UnknownToolError} if absent. */
  get(name) {
    const tool = this.tools.get(name);
    if (!tool) throw new UnknownToolError("tool", name);
    return tool;
  }
  /** The names of every registered tool, in registration order. */
  names() {
    return [...this.tools.keys()];
  }
  /**
   * Define (or redefine) a named collection as the given list of tool names.
   * Every listed tool must already be registered.
   */
  collection(name, toolNames) {
    for (const toolName of toolNames) {
      if (!this.tools.has(toolName)) throw new UnknownToolError("tool", toolName);
    }
    this.collections.set(name, [...toolNames]);
    return this;
  }
  /** The tool names belonging to a collection, or throw if it is undefined. */
  membersOf(collectionName) {
    const members = this.collections.get(collectionName);
    if (!members) throw new UnknownToolError("collection", collectionName);
    return [...members];
  }
  /** The names of every defined collection. */
  collectionNames() {
    return [...this.collections.keys()];
  }
  /**
   * Build a runtime {@link ToolBox} from a set of tools.
   *
   * Pass a collection name to box that collection, or omit it to box every
   * registered tool. `ctxFactory` is invoked once per dispatched call to mint
   * the {@link ToolContext} the chosen tool runs under.
   */
  toToolBox(ctxFactory, collectionName) {
    const selected = collectionName === void 0 ? [...this.tools.values()] : this.membersOf(collectionName).map((n) => this.get(n));
    const byName = new Map(selected.map((t) => [t.name, t]));
    const runner = {
      async run(call, signal) {
        const tool = byName.get(call.name);
        if (!tool) {
          return {
            id: call.id,
            output: `This session exposes no tool called "${call.name}".`,
            isError: true
          };
        }
        const base = ctxFactory();
        const ctx = { ...base, signal };
        return tool.invoke(call, ctx);
      }
    };
    return {
      descriptors() {
        return selected.map((t) => t.descriptor());
      },
      runner
    };
  }
};

// src/capabilities/backends/node-backends.ts
import { promises as fsp } from "node:fs";
import { spawn as childSpawn } from "node:child_process";
var NodeFs = class {
  async readFile(path, encoding) {
    if (encoding === void 0) {
      const buf = await fsp.readFile(path);
      return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
    }
    return fsp.readFile(path, { encoding });
  }
  async writeFile(path, data, encoding) {
    if (typeof data === "string") {
      await fsp.writeFile(path, data, { encoding: encoding ?? "utf8" });
      return;
    }
    await fsp.writeFile(path, data);
  }
  async stat(path) {
    const link = await fsp.lstat(path);
    const isSymlink = link.isSymbolicLink();
    const resolved = isSymlink ? await fsp.stat(path) : link;
    return {
      isFile: resolved.isFile(),
      isDirectory: resolved.isDirectory(),
      isSymlink,
      size: resolved.size,
      modifiedMs: resolved.mtimeMs
    };
  }
  async readdir(path) {
    const entries = await fsp.readdir(path, { withFileTypes: true });
    return entries.map((entry) => ({
      name: entry.name,
      isDirectory: entry.isDirectory()
    }));
  }
  async mkdir(path, options) {
    await fsp.mkdir(path, { recursive: options?.recursive ?? false });
  }
  async rm(path, options) {
    await fsp.rm(path, {
      recursive: options?.recursive ?? false,
      force: options?.ignoreMissing ?? false
    });
  }
  async exists(path) {
    try {
      await fsp.stat(path);
      return true;
    } catch {
      return false;
    }
  }
};
var nodeFs = new NodeFs();
function joinChunks(chunks) {
  return Buffer.concat(chunks).toString("utf8");
}
function asSignal(signal) {
  return signal;
}
var NodeShell = class {
  exec(command, options) {
    if (typeof command !== "string" || command.length === 0) {
      return Promise.reject(
        new Error("A shell command must be a non-empty string.")
      );
    }
    return new Promise((resolve, reject) => {
      const child = childSpawn(command, {
        shell: true,
        cwd: options?.cwd,
        env: options?.env ? { ...process.env, ...options.env } : process.env,
        signal: options?.signal
      });
      const out = [];
      const err = [];
      let settled = false;
      let timedOut = false;
      let timer;
      const finish = (fn) => {
        if (settled) return;
        settled = true;
        if (timer !== void 0) clearTimeout(timer);
        fn();
      };
      child.stdout?.on("data", (chunk) => out.push(chunk));
      child.stderr?.on("data", (chunk) => err.push(chunk));
      if (options?.input !== void 0 && child.stdin) {
        child.stdin.write(options.input);
        child.stdin.end();
      } else {
        child.stdin?.end();
      }
      const limit = options?.timeoutMs;
      if (limit !== void 0 && limit > 0) {
        timer = setTimeout(() => {
          timedOut = true;
          child.kill("SIGTERM");
        }, limit);
      }
      child.on("error", (cause) => {
        finish(() => reject(cause));
      });
      child.on("close", (code, signal) => {
        finish(() => {
          if (timedOut) {
            reject(
              new Error(
                `The command exceeded its ${limit}ms deadline and was stopped.`
              )
            );
            return;
          }
          resolve({
            stdout: joinChunks(out),
            stderr: joinChunks(err),
            // On a signalled exit Node reports a null code; preserve that so the
            // caller can tell "killed" apart from "exited 0".
            code: signal !== null ? null : code
          });
        });
      });
    });
  }
  spawn(command, options) {
    if (typeof command !== "string" || command.length === 0) {
      throw new Error("A shell command must be a non-empty string.");
    }
    const child = childSpawn(command, {
      shell: true,
      cwd: options?.cwd,
      env: options?.env ? { ...process.env, ...options.env } : process.env,
      signal: options?.signal
    });
    return new NodeShellHandle(child);
  }
};
var NodeShellHandle = class {
  child;
  listeners = /* @__PURE__ */ new Set();
  exited = false;
  exitCode = null;
  exitSignal = null;
  constructor(child) {
    this.child = child;
    child.once(
      "close",
      (code, signal) => {
        this.exited = true;
        this.exitCode = code;
        this.exitSignal = asSignal(signal);
        for (const listener of [...this.listeners]) {
          listener(this.exitCode, this.exitSignal);
        }
        this.listeners.clear();
      }
    );
  }
  get pid() {
    return this.child.pid;
  }
  kill(signal) {
    if (this.exited) return;
    this.child.kill(signal ?? "SIGTERM");
  }
  onExit(listener) {
    if (this.exited) {
      const code = this.exitCode;
      const signal = this.exitSignal;
      queueMicrotask(() => listener(code, signal));
      return () => {
      };
    }
    this.listeners.add(listener);
    return () => {
      this.listeners.delete(listener);
    };
  }
};
var nodeShell = new NodeShell();
function neverAborts() {
  return new AbortController().signal;
}
var standardBudget = {
  kind: "middle",
  maxBytes: 64 * 1024,
  notice: (omitted) => `
[${omitted} bytes elided to stay within the output ceiling]
`
};
function makeNodeContext(cwd, signal, budget, framework) {
  if (typeof cwd !== "string" || cwd.length === 0) {
    throw new Error("makeNodeContext requires a non-empty working directory.");
  }
  return {
    cwd,
    fs: nodeFs,
    shell: nodeShell,
    signal: signal ?? neverAborts(),
    budget: budget ?? standardBudget,
    ...framework ? { framework } : {}
  };
}

// src/capabilities/files/read-state-gate.ts
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
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.";
function getReadStateHandle(ctx) {
  const bag = ctx.framework;
  if (!bag || typeof bag !== "object") return void 0;
  const candidate = bag[READ_STATE_HANDLE_KEY];
  if (!candidate || typeof candidate !== "object") return void 0;
  const handle = candidate;
  if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
    return void 0;
  }
  return handle;
}
async function recordReadState(ctx, absPath, handle) {
  if (!handle) return;
  try {
    const info = await ctx.fs.stat(absPath);
    const record = {
      mtimeMs: Math.floor(info.modifiedMs),
      size: info.size,
      readAt: Date.now()
    };
    handle.set(absPath, record);
  } catch {
  }
}
async function enforceReadGate(ctx, absPath, handle) {
  if (!handle) return { ok: true };
  if (!handle.has(absPath)) {
    return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
  }
  const recorded = handle.get(absPath);
  if (!recorded) {
    return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
  }
  let info;
  try {
    info = await ctx.fs.stat(absPath);
  } catch {
    return { ok: true };
  }
  const currentMtime = Math.floor(info.modifiedMs);
  if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
    return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
  }
  return { ok: true };
}

// src/capabilities/files/read.ts
var GUTTER_WIDTH = 6;
var DESCRIPTION = [
  "Return the contents of a UTF-8 text file, with each line preceded by its",
  "1-based line number. Pass `offset` to begin partway down a large file and",
  "`limit` to cap how many lines come back; together they let you page through",
  "a file in chunks. Output is automatically bounded to fit the model context \u2014",
  "if the selected text is still too large it is shortened and a marker is left",
  "in place of the removed portion."
].join(" ");
function readPositiveInt(value, label) {
  if (value === void 0 || value === null) {
    return { ok: true, value: void 0 };
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    return { ok: false, reason: `${label} must be a number when provided.` };
  }
  if (!Number.isInteger(value)) {
    return { ok: false, reason: `${label} must be a whole number, not ${value}.` };
  }
  if (value < 1) {
    return { ok: false, reason: `${label} must be at least 1; got ${value}.` };
  }
  return { ok: true, value };
}
function failure(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function withGutter(slice, firstLineNumber) {
  const rendered = [];
  for (let i = 0; i < slice.length; i += 1) {
    const lineNo = firstLineNumber + i;
    const label = String(lineNo).padStart(GUTTER_WIDTH, " ");
    rendered.push(`${label}  ${slice[i]}`);
  }
  return rendered.join("\n");
}
function toLines(text) {
  if (text.length === 0) return [];
  const lines = text.split("\n");
  if (lines.length > 0 && lines[lines.length - 1] === "") {
    lines.pop();
  }
  return lines;
}
var readTool = defineTool({
  name: "read",
  description: DESCRIPTION,
  parameters: {
    type: "object",
    properties: {
      path: {
        type: "string",
        description: "Filesystem path of the text file to open."
      },
      offset: {
        type: "integer",
        minimum: 1,
        description: "1-based line number to start reading at. Defaults to the first line."
      },
      limit: {
        type: "integer",
        minimum: 1,
        description: "Greatest number of lines to return, counting from the start line."
      }
    },
    required: ["path"],
    additionalProperties: false
  },
  async run(input, ctx) {
    const raw = input;
    const bag = raw !== null && typeof raw === "object" ? raw : {};
    const rawPath = bag.path;
    if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
      return failure("A non-empty `path` string is required to read a file.");
    }
    const path = rawPath;
    const offsetParse = readPositiveInt(bag.offset, "offset");
    if (!offsetParse.ok) return failure(offsetParse.reason);
    const offset = offsetParse.value;
    const limitParse = readPositiveInt(bag.limit, "limit");
    if (!limitParse.ok) return failure(limitParse.reason);
    const limit = limitParse.value;
    let info;
    try {
      info = await ctx.fs.stat(path);
    } catch {
      return failure(`Nothing readable was found at ${path}.`);
    }
    if (info.isDirectory) {
      return failure(`${path} is a directory, so there is no file text to show.`);
    }
    if (!info.isFile) {
      return failure(`${path} is not a regular file and cannot be read as text.`);
    }
    let text;
    try {
      text = await ctx.fs.readFile(path, "utf8");
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      return failure(`Could not read ${path}: ${detail}`);
    }
    await recordReadState(ctx, path, getReadStateHandle(ctx));
    const allLines = toLines(text);
    const totalLines = allLines.length;
    if (totalLines === 0) {
      return {
        content: [{ kind: "text", text: `${path} is empty (no lines to show).` }]
      };
    }
    const startIndex = offset === void 0 ? 0 : offset - 1;
    if (startIndex >= totalLines) {
      return failure(
        `Start line ${offset} is past the end of ${path}, which has ${totalLines} line${totalLines === 1 ? "" : "s"}.`
      );
    }
    const endIndex = limit === void 0 ? totalLines : Math.min(totalLines, startIndex + limit);
    const windowLines = allLines.slice(startIndex, endIndex);
    const firstLineNumber = startIndex + 1;
    const lastLineNumber = endIndex;
    const windowed = startIndex > 0 || endIndex < totalLines;
    const gutterText = withGutter(windowLines, firstLineNumber);
    const clamped = clamp(gutterText, {
      kind: ctx.budget.kind,
      maxBytes: ctx.budget.maxBytes,
      notice: (omitted) => `
[read: ${omitted} more byte${omitted === 1 ? "" : "s"} of this file were left out to stay within the response budget]
`
    });
    const byteTrimmed = clamped !== gutterText;
    const headerParts = [];
    if (windowed) {
      headerParts.push(
        `Showing lines ${firstLineNumber}\u2013${lastLineNumber} of ${totalLines} in ${path}.`
      );
    } else {
      headerParts.push(
        `Showing all ${totalLines} line${totalLines === 1 ? "" : "s"} of ${path}.`
      );
    }
    if (windowed && lastLineNumber < totalLines) {
      const remaining = totalLines - lastLineNumber;
      headerParts.push(
        `${remaining} further line${remaining === 1 ? "" : "s"} follow; raise the start line to keep reading.`
      );
    }
    if (byteTrimmed) {
      headerParts.push(
        "This slice was additionally shortened to fit the response budget (see the inline marker)."
      );
    }
    const body = `${headerParts.join(" ")}

${clamped}`;
    return { content: [{ kind: "text", text: body }] };
  }
});

// src/capabilities/files/write.ts
var writeParameters = {
  type: "object",
  additionalProperties: false,
  required: ["path", "content"],
  properties: {
    path: {
      type: "string",
      description: "Where the file should be written. A relative path is taken against the session's working directory. Any missing parent folders are created automatically."
    },
    content: {
      type: "string",
      description: "The full text to store. An existing file at this path is replaced wholesale, not appended to."
    }
  }
};
function asText(text, failed = false) {
  return { content: [{ kind: "text", text }], isError: failed };
}
function readPath(raw) {
  if (typeof raw !== "string") {
    throw new Error("the 'path' argument must be a string naming the file to write");
  }
  if (raw.trim().length === 0) {
    throw new Error("the 'path' argument cannot be empty or whitespace-only");
  }
  return raw;
}
function readContent(raw) {
  if (raw === void 0 || raw === null) {
    return "";
  }
  if (typeof raw !== "string") {
    throw new Error("the 'content' argument must be a string of text to write");
  }
  return raw;
}
function parentDir(path) {
  const normalized = path.replace(/\\/g, "/");
  const cut = normalized.lastIndexOf("/");
  if (cut <= 0) {
    return "";
  }
  return normalized.slice(0, cut);
}
var writeTool = defineTool({
  name: "write",
  description: "Save a UTF-8 text file at the given path, overwriting any file already there. Missing parent directories are created first. Returns a short confirmation including the byte count that was written. Use this to produce or replace whole-file contents; it does not merge or patch.",
  parameters: writeParameters,
  async run(input, ctx) {
    const path = readPath(input?.path);
    const content = readContent(input?.content);
    const handle = getReadStateHandle(ctx);
    if (handle && await ctx.fs.exists(path)) {
      const gate = await enforceReadGate(ctx, path, handle);
      if (!gate.ok) {
        return asText(gate.message, true);
      }
    }
    const folder = parentDir(path);
    if (folder.length > 0) {
      await ctx.fs.mkdir(folder, { recursive: true });
    }
    await ctx.fs.writeFile(path, content, "utf8");
    await recordReadState(ctx, path, handle);
    const bytes = Buffer.byteLength(content, "utf8");
    const unit = bytes === 1 ? "byte" : "bytes";
    return asText(`Saved ${bytes} ${unit} to ${path}.`);
  }
});

// src/capabilities/files/diff.ts
function splitLines(text) {
  if (text.length === 0) return [""];
  const normalized = text.replace(/\r\n/g, "\n");
  const parts = normalized.split("\n");
  if (parts.length > 1 && parts[parts.length - 1] === "") {
    parts.pop();
  }
  return parts;
}
function computeTrace(a, b) {
  const n = a.length;
  const m = b.length;
  const max = n + m;
  const offset = max;
  const v = new Array(2 * max + 1).fill(0);
  const trace = [];
  for (let d = 0; d <= max; d++) {
    trace.push(v.slice());
    for (let k = -d; k <= d; k += 2) {
      let x;
      if (k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1]) {
        x = v[offset + k + 1];
      } else {
        x = v[offset + k - 1] + 1;
      }
      let y = x - k;
      while (x < n && y < m && a[x] === b[y]) {
        x++;
        y++;
      }
      v[offset + k] = x;
      if (x >= n && y >= m) {
        return trace;
      }
    }
  }
  return trace;
}
function backtrack(a, b, trace) {
  const n = a.length;
  const m = b.length;
  const offset = n + m;
  const ops = [];
  let x = n;
  let y = m;
  for (let d = trace.length - 1; d >= 0; d--) {
    const v = trace[d];
    const k = x - y;
    let prevK;
    if (k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1]) {
      prevK = k + 1;
    } else {
      prevK = k - 1;
    }
    const prevX = v[offset + prevK];
    const prevY = prevX - prevK;
    while (x > prevX && y > prevY) {
      ops.push({
        kind: "keep",
        text: a[x - 1],
        beforeLine: x,
        afterLine: y
      });
      x--;
      y--;
    }
    if (d > 0) {
      if (x === prevX) {
        ops.push({ kind: "add", text: b[prevY], afterLine: prevY + 1 });
      } else {
        ops.push({ kind: "remove", text: a[prevX], beforeLine: prevX + 1 });
      }
    }
    x = prevX;
    y = prevY;
  }
  ops.reverse();
  return ops;
}
function diffLines(before, after) {
  const a = splitLines(before);
  const b = splitLines(after);
  const trace = computeTrace(a, b);
  const ops = backtrack(a, b, trace);
  let added = 0;
  let removed = 0;
  for (const op of ops) {
    if (op.kind === "add") added++;
    else if (op.kind === "remove") removed++;
  }
  return { ops, added, removed };
}
function buildHunks(ops, context) {
  const hunks = [];
  let current = null;
  let trailingKeeps = 0;
  const openHunk = (atIndex) => {
    const lead = [];
    for (let i = atIndex - 1; i >= 0 && lead.length < context && ops[i].kind === "keep"; i--) {
      lead.unshift(ops[i]);
    }
    const first = lead[0] ?? ops[atIndex];
    const hunk = {
      beforeStart: first.beforeLine ?? 1,
      beforeCount: 0,
      afterStart: first.afterLine ?? 1,
      afterCount: 0,
      ops: [...lead]
    };
    return hunk;
  };
  for (let i = 0; i < ops.length; i++) {
    const op = ops[i];
    if (op.kind === "keep") {
      if (current) {
        if (trailingKeeps < context) {
          current.ops.push(op);
          trailingKeeps++;
        } else {
          hunks.push(current);
          current = null;
          trailingKeeps = 0;
        }
      }
      continue;
    }
    if (!current) {
      current = openHunk(i);
    }
    current.ops.push(op);
    trailingKeeps = 0;
  }
  if (current) hunks.push(current);
  for (const hunk of hunks) {
    let beforeCount = 0;
    let afterCount = 0;
    let beforeStart = 0;
    let afterStart = 0;
    for (const op of hunk.ops) {
      if (op.kind !== "add") {
        if (beforeStart === 0 && op.beforeLine) beforeStart = op.beforeLine;
        beforeCount++;
      }
      if (op.kind !== "remove") {
        if (afterStart === 0 && op.afterLine) afterStart = op.afterLine;
        afterCount++;
      }
    }
    hunk.beforeStart = beforeStart || (beforeCount === 0 ? 0 : 1);
    hunk.afterStart = afterStart || (afterCount === 0 ? 0 : 1);
    hunk.beforeCount = beforeCount;
    hunk.afterCount = afterCount;
  }
  return hunks;
}
function prefixFor(kind) {
  if (kind === "add") return "+";
  if (kind === "remove") return "-";
  return " ";
}
function renderUnifiedDiff(before, after, context = 3) {
  const { ops } = diffLines(before, after);
  const hasChange = ops.some((op) => op.kind !== "keep");
  if (!hasChange) return "";
  const hunks = buildHunks(ops, context);
  const lines = [];
  for (const hunk of hunks) {
    lines.push(
      `@@ -${hunk.beforeStart},${hunk.beforeCount} +${hunk.afterStart},${hunk.afterCount} @@`
    );
    for (const op of hunk.ops) {
      lines.push(prefixFor(op.kind) + op.text);
    }
  }
  return lines.join("\n");
}

// src/capabilities/files/edit.ts
var DIFF_BYTE_CEILING = 16e3;
function canonicalizeWhitespace(text) {
  return text.replace(/\s+/g, " ").trim();
}
function countLiteral(haystack, needle) {
  if (needle.length === 0) return 0;
  let count = 0;
  let from = 0;
  for (; ; ) {
    const at = haystack.indexOf(needle, from);
    if (at === -1) break;
    count++;
    from = at + needle.length;
  }
  return count;
}
function replaceAllLiteral(haystack, needle, value) {
  const pieces = [];
  let from = 0;
  for (; ; ) {
    const at = haystack.indexOf(needle, from);
    if (at === -1) {
      pieces.push(haystack.slice(from));
      break;
    }
    pieces.push(haystack.slice(from, at), value);
    from = at + needle.length;
  }
  return pieces.join("");
}
function findFuzzySpans(source, target) {
  const wantCanonical = canonicalizeWhitespace(target);
  if (wantCanonical.length === 0) return [];
  const isSpace = (ch) => /\s/.test(ch);
  const spans = [];
  for (let start = 0; start < source.length; start++) {
    if (isSpace(source[start])) continue;
    for (let end = start + 1; end <= source.length; end++) {
      const windowCanonical = canonicalizeWhitespace(source.slice(start, end));
      if (windowCanonical.length > wantCanonical.length) {
        break;
      }
      if (windowCanonical === wantCanonical) {
        let trimmedEnd = end;
        while (trimmedEnd > start && isSpace(source[trimmedEnd - 1])) {
          trimmedEnd--;
        }
        spans.push({ start, end: trimmedEnd });
        start = trimmedEnd - 1;
        break;
      }
    }
  }
  return spans;
}
function failure2(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function success(path, before, after, replacements) {
  const rawDiff = renderUnifiedDiff(before, after);
  const diff = clamp(rawDiff, {
    kind: "middle",
    maxBytes: DIFF_BYTE_CEILING,
    notice: (omitted) => `
\u2026 diff shortened, ${omitted} byte(s) of the middle omitted \u2026
`
  });
  const plural = replacements === 1 ? "" : "s";
  const summary = `Updated ${path} \u2014 applied ${replacements} replacement${plural}.`;
  const json = {
    kind: "json",
    value: { path, replacements, diff }
  };
  return { content: [{ kind: "text", text: summary }, json] };
}
async function runEdit(input, ctx) {
  if (typeof input !== "object" || input === null) {
    return failure2("The edit tool expects an object with path, oldText, and newText.");
  }
  const { path, oldText, newText } = input;
  if (typeof path !== "string" || path.trim().length === 0) {
    return failure2("Provide a non-empty `path` string naming the file to edit.");
  }
  if (typeof oldText !== "string") {
    return failure2("`oldText` must be a string holding the snippet to replace.");
  }
  if (typeof newText !== "string") {
    return failure2("`newText` must be a string holding the replacement snippet.");
  }
  if (oldText.length === 0) {
    return failure2("`oldText` cannot be empty \u2014 give the exact text you want replaced.");
  }
  if (oldText === newText) {
    return failure2("`oldText` and `newText` are identical, so there is nothing to change.");
  }
  const replaceAll = input.replaceAll === true;
  const present = await ctx.fs.exists(path);
  if (!present) {
    return failure2(`No file exists at ${path}; nothing to edit.`);
  }
  const info = await ctx.fs.stat(path);
  if (!info.isFile) {
    return failure2(`${path} is not a regular file, so it cannot be edited.`);
  }
  const handle = getReadStateHandle(ctx);
  const gate = await enforceReadGate(ctx, path, handle);
  if (!gate.ok) {
    return failure2(gate.message);
  }
  const before = await ctx.fs.readFile(path, "utf8");
  const literalHits = countLiteral(before, oldText);
  if (literalHits > 0) {
    if (!replaceAll && literalHits > 1) {
      return failure2(
        `Found ${literalHits} exact matches for the snippet in ${path}. Pass replaceAll: true to change them all, or widen \`oldText\` so it pinpoints a single location.`
      );
    }
    const after2 = replaceAll ? replaceAllLiteral(before, oldText, newText) : before.replace(oldText, newText);
    if (after2 === before) {
      return failure2(`The replacement left ${path} unchanged.`);
    }
    await ctx.fs.writeFile(path, after2, "utf8");
    await recordReadState(ctx, path, handle);
    return success(path, before, after2, replaceAll ? literalHits : 1);
  }
  const spans = findFuzzySpans(before, oldText);
  if (spans.length === 0) {
    return failure2(
      `Could not locate the snippet in ${path}, even after ignoring whitespace differences. Re-check \`oldText\` against the file's current contents.`
    );
  }
  if (!replaceAll && spans.length > 1) {
    return failure2(
      `The snippet matches ${spans.length} regions of ${path} once whitespace is ignored. Pass replaceAll: true, or extend \`oldText\` to target one region.`
    );
  }
  const targets = replaceAll ? spans : [spans[0]];
  let after = before;
  for (let i = targets.length - 1; i >= 0; i--) {
    const { start, end } = targets[i];
    after = after.slice(0, start) + newText + after.slice(end);
  }
  if (after === before) {
    return failure2(`The fuzzy replacement left ${path} unchanged.`);
  }
  await ctx.fs.writeFile(path, after, "utf8");
  await recordReadState(ctx, path, handle);
  return success(path, before, after, targets.length);
}
var editTool = defineTool({
  name: "edit",
  description: "Replace a snippet of text inside one file. By default the snippet (`oldText`) must appear exactly once; if no exact match is found, a single whitespace-tolerant match is attempted. Set `replaceAll` to change every occurrence. Returns a unified-style diff of what changed. The edit is refused when the snippet is missing or matches ambiguously.",
  parameters: {
    type: "object",
    additionalProperties: false,
    required: ["path", "oldText", "newText"],
    properties: {
      path: {
        type: "string",
        description: "Path to the file to edit, relative to the working directory or absolute."
      },
      oldText: {
        type: "string",
        description: "The exact text to find and replace. Must be non-empty."
      },
      newText: {
        type: "string",
        description: "The text to put in place of `oldText`."
      },
      replaceAll: {
        type: "boolean",
        description: "Replace every occurrence instead of requiring a single unique match. Defaults to false."
      }
    }
  },
  run: runEdit
});

// src/capabilities/files/ls.ts
var lsParameters = {
  type: "object",
  additionalProperties: false,
  properties: {
    path: {
      type: "string",
      description: "Directory whose immediate contents should be listed. Leave this out to enumerate the session's current working directory."
    },
    showHidden: {
      type: "boolean",
      description: "Set to true to also surface dot-prefixed entries (such as `.git` or `.env`), which are omitted otherwise."
    }
  },
  required: []
};
var DESCRIPTION2 = [
  "List the direct children of a directory, reporting each entry's name, its",
  "kind (file, directory, symlink, or other), and its size in bytes. Only the",
  "immediate level is shown \u2014 this does not descend into subdirectories. Pass",
  "`path` to point at a specific folder, or omit it to read the working",
  "directory; set `showHidden` to true to include dot-prefixed entries. Long",
  "listings are trimmed to stay within the response budget."
].join(" ");
function failure3(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function isHiddenName(name) {
  return name.startsWith(".");
}
function compareEntries(a, b) {
  const aDir = a.kind === "directory";
  const bDir = b.kind === "directory";
  if (aDir !== bDir) return aDir ? -1 : 1;
  const aKey = a.name.toLowerCase();
  const bKey = b.name.toLowerCase();
  if (aKey < bKey) return -1;
  if (aKey > bKey) return 1;
  if (a.name < b.name) return -1;
  if (a.name > b.name) return 1;
  return 0;
}
function renderRow(entry, kindWidth, sizeWidth) {
  const kindCell = entry.kind.padEnd(kindWidth, " ");
  const sizeCell = String(entry.size).padStart(sizeWidth, " ");
  const display = entry.kind === "directory" ? `${entry.name}/` : entry.name;
  return `${kindCell}  ${sizeCell}  ${display}`;
}
var lsTool = defineTool({
  name: "ls",
  description: DESCRIPTION2,
  parameters: lsParameters,
  async run(input, ctx) {
    const raw = input;
    const bag = raw !== null && typeof raw === "object" ? raw : {};
    const rawPath = bag.path;
    let path;
    if (rawPath === void 0 || rawPath === null) {
      path = ctx.cwd;
    } else if (typeof rawPath === "string") {
      const trimmed = rawPath.trim();
      path = trimmed.length === 0 ? ctx.cwd : rawPath;
    } else {
      return failure3("`path` must be a string when provided.");
    }
    const rawShowHidden = bag.showHidden;
    let showHidden = false;
    if (rawShowHidden !== void 0 && rawShowHidden !== null) {
      if (typeof rawShowHidden !== "boolean") {
        return failure3("`showHidden` must be a boolean when provided.");
      }
      showHidden = rawShowHidden;
    }
    let info;
    try {
      info = await ctx.fs.stat(path);
    } catch {
      return failure3(`No directory was found at ${path}.`);
    }
    if (!info.isDirectory) {
      return failure3(`${path} is not a directory, so it cannot be listed.`);
    }
    let children;
    try {
      children = await ctx.fs.readdir(path);
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      return failure3(`Could not read the contents of ${path}: ${detail}`);
    }
    const entries = [];
    let hiddenSkipped = 0;
    for (const child of children) {
      if (!showHidden && isHiddenName(child.name)) {
        hiddenSkipped += 1;
        continue;
      }
      const childPath = joinPath(path, child.name);
      let kind;
      let size = 0;
      try {
        const childInfo = await ctx.fs.stat(childPath);
        if (childInfo.isSymlink) {
          kind = "symlink";
        } else if (childInfo.isDirectory) {
          kind = "directory";
        } else if (childInfo.isFile) {
          kind = "file";
        } else {
          kind = "other";
        }
        size = childInfo.isDirectory ? 0 : childInfo.size;
      } catch {
        kind = child.isDirectory ? "directory" : "other";
        size = 0;
      }
      entries.push({ name: child.name, kind, size });
    }
    if (entries.length === 0) {
      const note = hiddenSkipped > 0 ? ` (${hiddenSkipped} hidden entr${hiddenSkipped === 1 ? "y was" : "ies were"} omitted; pass showHidden to reveal them)` : "";
      return {
        content: [
          { kind: "text", text: `${path} contains no listable entries${note}.` }
        ]
      };
    }
    entries.sort(compareEntries);
    let kindWidth = 0;
    let sizeWidth = 0;
    for (const entry of entries) {
      if (entry.kind.length > kindWidth) kindWidth = entry.kind.length;
      const sizeLen = String(entry.size).length;
      if (sizeLen > sizeWidth) sizeWidth = sizeLen;
    }
    const rows = entries.map((entry) => renderRow(entry, kindWidth, sizeWidth));
    const table2 = rows.join("\n");
    const clamped = clamp(table2, {
      kind: ctx.budget.kind,
      maxBytes: ctx.budget.maxBytes,
      notice: (omitted) => `
[ls: ${omitted} more byte${omitted === 1 ? "" : "s"} of this listing were withheld to fit the response budget]
`
    });
    const byteTrimmed = clamped !== table2;
    const headerParts = [];
    headerParts.push(
      `${entries.length} entr${entries.length === 1 ? "y" : "ies"} in ${path}:`
    );
    if (!showHidden && hiddenSkipped > 0) {
      headerParts.push(
        `(${hiddenSkipped} hidden entr${hiddenSkipped === 1 ? "y" : "ies"} not shown; pass showHidden to include them.)`
      );
    }
    if (byteTrimmed) {
      headerParts.push(
        "This listing was shortened to fit the response budget (see the inline marker)."
      );
    }
    const body = `${headerParts.join(" ")}

${clamped}`;
    return { content: [{ kind: "text", text: body }] };
  }
});
function joinPath(dir, name) {
  if (dir.length === 0) return name;
  const last = dir[dir.length - 1];
  if (last === "/" || last === "\\") {
    return `${dir}${name}`;
  }
  return `${dir}/${name}`;
}

// src/capabilities/search/grep.ts
var PRUNED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
var DEFAULT_HIT_CAP = 200;
var ABSOLUTE_HIT_CAP = 5e3;
var MAX_FILE_BYTES = 2 * 1024 * 1024;
var BadRequest = class extends Error {
};
function asPositiveInt(value) {
  if (typeof value !== "number" || !Number.isFinite(value)) return null;
  const floored = Math.floor(value);
  return floored > 0 ? floored : null;
}
function sanitizeFlags(flags) {
  if (flags === void 0 || flags === null) return "";
  if (typeof flags !== "string") {
    throw new BadRequest(
      'The `flags` argument, when present, must be a string of letters such as "i" or "m".'
    );
  }
  if (!/^[gimsuy]*$/.test(flags)) {
    throw new BadRequest(
      `Unrecognized regex flag in "${flags}"; only the letters g, i, m, s, u and y are accepted.`
    );
  }
  return flags;
}
function compile(pattern, flags) {
  try {
    return new RegExp(pattern, flags);
  } catch (err) {
    const why = err instanceof Error ? err.message : String(err);
    throw new BadRequest(`Could not compile the search pattern: ${why}`);
  }
}
function looksBinary(text) {
  const limit = Math.min(text.length, 4096);
  for (let i = 0; i < limit; i++) {
    if (text.charCodeAt(i) === 0) return true;
  }
  return false;
}
function relativize(root, full) {
  if (full === root) return full;
  const prefix = root.endsWith("/") ? root : root + "/";
  return full.startsWith(prefix) ? full.slice(prefix.length) : full;
}
function joinPath2(parent, child) {
  return parent.endsWith("/") ? parent + child : parent + "/" + child;
}
async function scanFile(ctx, root, full, matcher, cap, sink) {
  let body;
  try {
    body = await ctx.fs.readFile(full, "utf8");
  } catch {
    return false;
  }
  if (looksBinary(body)) return false;
  const lines = body.split("\n");
  const shown = relativize(root, full);
  for (let i = 0; i < lines.length; i++) {
    if (ctx.signal.aborted) return true;
    matcher.lastIndex = 0;
    if (matcher.test(lines[i])) {
      sink.push({ path: shown, line: i + 1, text: lines[i] });
      if (sink.length >= cap) return true;
    }
  }
  return false;
}
async function walk(ctx, root, dir, matcher, cap, sink) {
  if (ctx.signal.aborted) return true;
  let children;
  try {
    children = await ctx.fs.readdir(dir);
  } catch {
    return false;
  }
  children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
  for (const child of children) {
    if (ctx.signal.aborted) return true;
    const full = joinPath2(dir, child.name);
    if (child.isDirectory) {
      if (PRUNED_DIRS.has(child.name)) continue;
      if (await walk(ctx, root, full, matcher, cap, sink)) return true;
      continue;
    }
    try {
      const info = await ctx.fs.stat(full);
      if (info.size > MAX_FILE_BYTES) continue;
    } catch {
    }
    if (await scanFile(ctx, root, full, matcher, cap, sink)) return true;
  }
  return false;
}
function renderReport(hits, pattern, cap) {
  if (hits.length === 0) {
    return `No lines matched /${pattern}/.`;
  }
  const lines = hits.map((h) => `${h.path}:${h.line}: ${h.text}`);
  const header = hits.length >= cap ? `Reached the ${cap}-hit limit; showing the first ${hits.length} matches:` : `Found ${hits.length} matching line${hits.length === 1 ? "" : "s"}:`;
  return `${header}
${lines.join("\n")}`;
}
var grepTool = defineTool({
  name: "grep",
  description: 'Search the contents of files for a regular expression. Walks the given path recursively (skipping node_modules and .git), tests each line of every readable text file, and returns matches as `path:line` plus the matching line. Provide `pattern` (a JS regex), and optionally `path` to root the search, `flags` (e.g. "i" for case-insensitive), and `maxMatches` to cap the number of hits. Use this to find where a symbol, string, or phrase appears across a codebase.',
  parameters: {
    type: "object",
    additionalProperties: false,
    required: ["pattern"],
    properties: {
      pattern: {
        type: "string",
        description: "The regular expression matched against each line of file content."
      },
      path: {
        type: "string",
        description: "File or directory to search beneath; defaults to the working directory."
      },
      flags: {
        type: "string",
        description: "JS regex flag letters (g, i, m, s, u, y); omit for none."
      },
      maxMatches: {
        type: "integer",
        minimum: 1,
        description: "Upper bound on reported matches. Defaults to a sensible cap."
      }
    }
  },
  async run(input, ctx) {
    try {
      if (input === null || typeof input !== "object") {
        throw new BadRequest(
          "Expected an object of arguments with at least a `pattern` field."
        );
      }
      const pattern = input.pattern;
      if (typeof pattern !== "string" || pattern.length === 0) {
        throw new BadRequest(
          "The `pattern` argument is required and must be a non-empty string."
        );
      }
      const flags = sanitizeFlags(input.flags);
      const matcher = compile(pattern, flags);
      const requested = asPositiveInt(input.maxMatches);
      const cap = Math.min(requested ?? DEFAULT_HIT_CAP, ABSOLUTE_HIT_CAP);
      let rawPath = ctx.cwd;
      if (input.path !== void 0 && input.path !== null) {
        if (typeof input.path !== "string") {
          throw new BadRequest("The `path` argument, when present, must be a string.");
        }
        if (input.path.length > 0) rawPath = input.path;
      }
      let rootInfo;
      try {
        rootInfo = await ctx.fs.stat(rawPath);
      } catch {
        return {
          content: [
            {
              kind: "text",
              text: `Nothing to search: the path "${rawPath}" does not exist or is unreadable.`
            }
          ],
          isError: true
        };
      }
      const hits = [];
      if (rootInfo.isDirectory) {
        await walk(ctx, rawPath, rawPath, matcher, cap, hits);
      } else if (rootInfo.isFile) {
        await scanFile(ctx, rawPath, rawPath, matcher, cap, hits);
      } else {
        return {
          content: [
            {
              kind: "text",
              text: `The path "${rawPath}" is neither a file nor a directory; nothing was searched.`
            }
          ],
          isError: true
        };
      }
      if (ctx.signal.aborted) {
        return {
          content: [{ kind: "text", text: "Search was cancelled before it finished." }],
          isError: true
        };
      }
      const report = renderReport(hits, pattern, cap);
      return { content: [{ kind: "text", text: clamp(report, ctx.budget) }] };
    } catch (err) {
      if (err instanceof BadRequest) {
        return { content: [{ kind: "text", text: err.message }], isError: true };
      }
      throw err;
    }
  }
});

// src/capabilities/search/find.ts
var PRUNED_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
var DEFAULT_RESULT_CAP = 500;
var ABSOLUTE_RESULT_CAP = 1e4;
var BadRequest2 = class extends Error {
};
function escapeRegex(literal) {
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function looksGlob(query) {
  return query.includes("*") || query.includes("?");
}
function compileGlob(query) {
  let body = "";
  for (const ch of query) {
    if (ch === "*") {
      body += ".*";
    } else if (ch === "?") {
      body += ".";
    } else {
      body += escapeRegex(ch);
    }
  }
  try {
    return new RegExp(`^${body}$`, "i");
  } catch (err) {
    const why = err instanceof Error ? err.message : String(err);
    throw new BadRequest2(`The glob "${query}" could not be turned into a matcher: ${why}`);
  }
}
function buildMatcher(query) {
  if (looksGlob(query)) {
    const re = compileGlob(query);
    return (name) => re.test(name);
  }
  const needle = query.toLowerCase();
  return (name) => name.toLowerCase().includes(needle);
}
function readKind(value) {
  if (value === void 0 || value === null) return void 0;
  if (value !== "file" && value !== "dir") {
    throw new BadRequest2('The `type` argument, when present, must be exactly "file" or "dir".');
  }
  return value;
}
function relativize2(root, full) {
  if (full === root) return ".";
  const prefix = root.endsWith("/") ? root : root + "/";
  return full.startsWith(prefix) ? full.slice(prefix.length) : full;
}
function joinPath3(parent, child) {
  return parent.endsWith("/") ? parent + child : parent + "/" + child;
}
async function walk2(ctx, root, dir, matcher, wanted, cap, sink) {
  if (ctx.signal.aborted) return true;
  let children;
  try {
    children = await ctx.fs.readdir(dir);
  } catch {
    return false;
  }
  children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
  for (const child of children) {
    if (ctx.signal.aborted) return true;
    const full = joinPath3(dir, child.name);
    const kindMatches = wanted === void 0 || (wanted === "dir" ? child.isDirectory : !child.isDirectory);
    if (kindMatches && matcher(child.name)) {
      sink.push(relativize2(root, full));
      if (sink.length >= cap) return true;
    }
    if (child.isDirectory) {
      if (PRUNED_DIRS2.has(child.name)) continue;
      if (await walk2(ctx, root, full, matcher, wanted, cap, sink)) return true;
    }
  }
  return false;
}
function renderReport2(hits, query, cap) {
  if (hits.length === 0) {
    return `Nothing matched "${query}".`;
  }
  const header = hits.length >= cap ? `Hit the ${cap}-entry limit; listing the first ${hits.length} matches:` : `Matched ${hits.length} entr${hits.length === 1 ? "y" : "ies"}:`;
  return `${header}
${hits.join("\n")}`;
}
var findTool = defineTool({
  name: "find",
  description: 'Locate files and folders by name beneath a path. Walks the given directory recursively (skipping node_modules and .git) and reports every entry whose name matches `query`. The query is treated as a glob when it contains `*` or `?` (anchored to the whole name, case-insensitive), and otherwise as a case-insensitive substring. Optionally pass `path` to root the search and `type` ("file" or "dir") to keep only one kind of entry. Returns matching paths relative to the search root. Use this to find where a file lives when you know part of its name rather than its contents.',
  parameters: {
    type: "object",
    additionalProperties: false,
    required: ["query"],
    properties: {
      query: {
        type: "string",
        description: "Glob (with `*`/`?`, matched against the whole name) or plain substring to match entry names against."
      },
      path: {
        type: "string",
        description: "Directory to search beneath; defaults to the working directory."
      },
      type: {
        type: "string",
        enum: ["file", "dir"],
        description: 'Restrict results to regular files ("file") or directories ("dir"); omit for both.'
      }
    }
  },
  async run(input, ctx) {
    try {
      if (input === null || typeof input !== "object") {
        throw new BadRequest2("Expected an object of arguments with at least a `query` field.");
      }
      const query = input.query;
      if (typeof query !== "string" || query.length === 0) {
        throw new BadRequest2("The `query` argument is required and must be a non-empty string.");
      }
      const wanted = readKind(input.type);
      const matcher = buildMatcher(query);
      let rawPath = ctx.cwd;
      if (input.path !== void 0 && input.path !== null) {
        if (typeof input.path !== "string") {
          throw new BadRequest2("The `path` argument, when present, must be a string.");
        }
        if (input.path.length > 0) rawPath = input.path;
      }
      let rootInfo;
      try {
        rootInfo = await ctx.fs.stat(rawPath);
      } catch {
        return {
          content: [
            {
              kind: "text",
              text: `Cannot search: the path "${rawPath}" does not exist or is unreadable.`
            }
          ],
          isError: true
        };
      }
      if (!rootInfo.isDirectory) {
        return {
          content: [
            {
              kind: "text",
              text: `The path "${rawPath}" is not a directory, so there is nothing to walk.`
            }
          ],
          isError: true
        };
      }
      const cap = Math.min(DEFAULT_RESULT_CAP, ABSOLUTE_RESULT_CAP);
      const hits = [];
      await walk2(ctx, rawPath, rawPath, matcher, wanted, cap, hits);
      if (ctx.signal.aborted) {
        return {
          content: [{ kind: "text", text: "The search was cancelled before it finished." }],
          isError: true
        };
      }
      const report = renderReport2(hits, query, cap);
      return { content: [{ kind: "text", text: clamp(report, ctx.budget) }] };
    } catch (err) {
      if (err instanceof BadRequest2) {
        return { content: [{ kind: "text", text: err.message }], isError: true };
      }
      throw err;
    }
  }
});

// src/capabilities/shell/bash.ts
var DEADLINE_CEILING_MS = 10 * 60 * 1e3;
var DEFAULT_DEADLINE_MS = 2 * 60 * 1e3;
var PARAMETERS = {
  type: "object",
  additionalProperties: false,
  required: ["command"],
  properties: {
    command: {
      type: "string",
      description: "The shell command line to run, exactly as you would type it at a prompt."
    },
    timeoutMs: {
      type: "number",
      description: "Optional time limit in milliseconds. The command is terminated once it elapses; omit it to use the default window."
    },
    cwd: {
      type: "string",
      description: "Optional directory to run the command in. Defaults to the session's current working directory when left out."
    }
  }
};
var DESCRIPTION3 = [
  "Run one shell command and get back its combined output and exit status.",
  "The command runs to completion (subject to an optional time limit), and you receive its standard output, standard error, and the numeric exit code.",
  "Use this for build steps, test runs, version-control queries, and other one-off shell work; it is not a persistent session, so each call starts fresh."
].join(" ");
function readInput(raw) {
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
    throw new Error("Expected an arguments object carrying at least a `command` string.");
  }
  const bag = raw;
  const command = bag.command;
  if (typeof command !== "string") {
    throw new Error("The `command` argument is required and must be a string.");
  }
  if (command.trim().length === 0) {
    throw new Error("The `command` argument cannot be empty or whitespace only.");
  }
  let timeoutMs;
  if (bag.timeoutMs !== void 0) {
    const raw2 = bag.timeoutMs;
    if (typeof raw2 !== "number" || !Number.isFinite(raw2) || raw2 <= 0) {
      throw new Error("When given, `timeoutMs` must be a positive, finite number of milliseconds.");
    }
    timeoutMs = Math.min(Math.floor(raw2), DEADLINE_CEILING_MS);
  }
  let cwd;
  if (bag.cwd !== void 0) {
    if (typeof bag.cwd !== "string" || bag.cwd.trim().length === 0) {
      throw new Error("When given, `cwd` must be a non-empty directory path.");
    }
    cwd = bag.cwd;
  }
  return { command, timeoutMs, cwd };
}
function renderReport3(stdout, stderr, code) {
  const sections = [];
  if (stdout.length > 0) {
    sections.push(`stdout:
${stdout}`);
  }
  if (stderr.length > 0) {
    sections.push(`stderr:
${stderr}`);
  }
  if (sections.length === 0) {
    sections.push("(the command produced no output)");
  }
  const status = code === null ? "status: the command was halted by a signal before it could return an exit code" : `status: exited with code ${code}`;
  sections.push(status);
  return sections.join("\n\n");
}
async function run(input, ctx) {
  const deadline = input.timeoutMs ?? DEFAULT_DEADLINE_MS;
  const result = await ctx.shell.exec(input.command, {
    cwd: input.cwd ?? ctx.cwd,
    signal: ctx.signal,
    timeoutMs: deadline
  });
  const report = renderReport3(result.stdout, result.stderr, result.code);
  const windowed = clamp(report, ctx.budget);
  const failed = result.code === null || result.code !== 0;
  return {
    content: [{ kind: "text", text: windowed }],
    isError: failed
  };
}
var bashTool = defineTool({
  name: "bash",
  description: DESCRIPTION3,
  parameters: PARAMETERS,
  async run(input, ctx) {
    const parsed = readInput(input);
    return run(parsed, ctx);
  }
});

// src/capabilities/shell/process.ts
var ProcessTable = class {
  jobs = /* @__PURE__ */ new Map();
  counter = 0;
  /** Mint a fresh, collision-free job id. */
  nextId() {
    this.counter += 1;
    return `job-${this.counter}`;
  }
  add(entry) {
    this.jobs.set(entry.id, entry);
  }
  get(id) {
    return this.jobs.get(id);
  }
  remove(id) {
    const entry = this.jobs.get(id);
    if (entry !== void 0) {
      this.jobs.delete(id);
    }
    return entry;
  }
  all() {
    return [...this.jobs.values()];
  }
  /**
   * Empty the table. Any job still running is killed and its exit listener
   * dropped, so a test (or a host shutting down) leaves nothing dangling.
   */
  reset() {
    for (const entry of this.jobs.values()) {
      entry.unsubscribe();
      if (entry.state === "running") {
        try {
          entry.handle.kill();
        } catch {
        }
      }
    }
    this.jobs.clear();
    this.counter = 0;
  }
};
var table = new ProcessTable();
function resetProcessTable() {
  table.reset();
}
var POLL_WINDOW_BYTES = 32 * 1024;
var SCRATCH_DIR = ".indus-process";
function joinPath4(base, tail) {
  if (base.length === 0) return tail;
  const last = base[base.length - 1];
  if (last === "/" || last === "\\") return `${base}${tail}`;
  return `${base}/${tail}`;
}
var processParameters = {
  type: "object",
  additionalProperties: false,
  required: ["action"],
  properties: {
    action: {
      type: "string",
      enum: ["start", "list", "poll", "stop"],
      description: "Which operation to run: 'start' a new background command, 'list' all tracked jobs, 'poll' a job for output it has printed since you last looked, or 'stop' a job and discard it."
    },
    command: {
      type: "string",
      description: "The shell command line to launch in the background. Required for 'start' and ignored by the other actions."
    },
    id: {
      type: "string",
      description: "The identifier of the job to act on, as returned by 'start'. Required for 'poll' and 'stop'."
    }
  }
};
var DESCRIPTION4 = [
  "Manage commands that keep running in the background across turns.",
  "Unlike a one-shot shell call, a started job stays alive so you can launch a server, watcher, or long task and check on it later.",
  "Use action='start' with a command to launch one and get a job id back; 'list' to see every job and its status; 'poll' with the id to read whatever output has accumulated since your last poll; and 'stop' with the id to terminate it."
].join(" ");
function asText2(text, failed = false) {
  return { content: [{ kind: "text", text }], isError: failed };
}
function asJson(value) {
  return { content: [{ kind: "json", value }] };
}
function readInput2(raw) {
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
    throw new Error(
      "Expected an arguments object with an 'action' of start, list, poll, or stop."
    );
  }
  const bag = raw;
  const action = bag.action;
  if (action !== "start" && action !== "list" && action !== "poll" && action !== "stop") {
    throw new Error(
      "The 'action' argument must be one of: start, list, poll, stop."
    );
  }
  let command;
  if (bag.command !== void 0) {
    if (typeof bag.command !== "string") {
      throw new Error("When given, 'command' must be a string command line.");
    }
    command = bag.command;
  }
  let id;
  if (bag.id !== void 0) {
    if (typeof bag.id !== "string") {
      throw new Error("When given, 'id' must be a string job identifier.");
    }
    id = bag.id;
  }
  return { action, command, id };
}
function shellQuote(value) {
  return `'${value.replace(/'/g, `'\\''`)}'`;
}
async function drainCapture(ctx, path, alreadyRead) {
  let full;
  try {
    full = await ctx.fs.readFile(path, "utf8");
  } catch {
    return { fresh: "", total: alreadyRead };
  }
  const total = Buffer.byteLength(full, "utf8");
  if (total <= alreadyRead) {
    return { fresh: "", total };
  }
  const fresh = Buffer.from(full, "utf8").subarray(alreadyRead).toString("utf8");
  return { fresh, total };
}
function describeStatus(entry) {
  if (entry.state === "running") {
    const pid = entry.pid === void 0 ? "unknown pid" : `pid ${entry.pid}`;
    return `running (${pid})`;
  }
  if (entry.exitSignal !== null) {
    return `stopped by signal ${entry.exitSignal}`;
  }
  if (entry.exitCode === null) {
    return "finished (no exit code reported)";
  }
  return `exited with code ${entry.exitCode}`;
}
async function startJob(ctx, command) {
  const id = table.nextId();
  const scratchDir = joinPath4(ctx.cwd, SCRATCH_DIR);
  const outPath = joinPath4(scratchDir, `${id}.out`);
  const errPath = joinPath4(scratchDir, `${id}.err`);
  await ctx.fs.mkdir(scratchDir, { recursive: true });
  await ctx.fs.writeFile(outPath, "", "utf8");
  await ctx.fs.writeFile(errPath, "", "utf8");
  const wrapped = `( ${command} ) >${shellQuote(outPath)} 2>${shellQuote(errPath)}`;
  const handle = ctx.shell.spawn(wrapped, { cwd: ctx.cwd });
  const entry = {
    id,
    command,
    handle,
    pid: handle.pid,
    outPath,
    errPath,
    startedMs: Date.now(),
    state: "running",
    exitCode: null,
    exitSignal: null,
    readOut: 0,
    readErr: 0,
    unsubscribe: () => {
    }
  };
  const unsubscribe = handle.onExit((code, signal) => {
    entry.state = "finished";
    entry.exitCode = code;
    entry.exitSignal = signal;
  });
  entry.unsubscribe = unsubscribe;
  table.add(entry);
  return asJson({
    action: "start",
    id,
    pid: entry.pid ?? null,
    command,
    status: describeStatus(entry),
    note: `Started job ${id}. Use action='poll' with this id to read its output.`
  });
}
function listJobs() {
  const entries = table.all();
  if (entries.length === 0) {
    return asJson({
      action: "list",
      jobs: [],
      note: "No background jobs are currently tracked."
    });
  }
  const jobs = entries.map((entry) => ({
    id: entry.id,
    command: entry.command,
    pid: entry.pid ?? null,
    state: entry.state,
    status: describeStatus(entry),
    ageMs: Date.now() - entry.startedMs
  }));
  return asJson({ action: "list", jobs });
}
async function pollJob(ctx, id) {
  const entry = table.get(id);
  if (entry === void 0) {
    return asText2(
      `No background job is tracked under id '${id}'. Use action='list' to see active jobs.`,
      true
    );
  }
  const out = await drainCapture(ctx, entry.outPath, entry.readOut);
  const err = await drainCapture(ctx, entry.errPath, entry.readErr);
  entry.readOut = out.total;
  entry.readErr = err.total;
  const window = { kind: "tail", maxBytes: POLL_WINDOW_BYTES };
  const stdout = clamp(out.fresh, window);
  const stderr = clamp(err.fresh, window);
  return asJson({
    action: "poll",
    id: entry.id,
    state: entry.state,
    status: describeStatus(entry),
    exitCode: entry.exitCode,
    exitSignal: entry.exitSignal,
    stdout,
    stderr,
    hadNewOutput: out.fresh.length > 0 || err.fresh.length > 0
  });
}
function stopJob(id) {
  const entry = table.remove(id);
  if (entry === void 0) {
    return asText2(
      `No background job is tracked under id '${id}'. It may have already been stopped.`,
      true
    );
  }
  entry.unsubscribe();
  const wasRunning = entry.state === "running";
  if (wasRunning) {
    try {
      entry.handle.kill();
    } catch {
    }
  }
  return asJson({
    action: "stop",
    id: entry.id,
    wasRunning,
    finalStatus: wasRunning ? "signalled to terminate and removed" : describeStatus(entry)
  });
}
var processTool = defineTool({
  name: "process",
  description: DESCRIPTION4,
  parameters: processParameters,
  async run(input, ctx) {
    const parsed = readInput2(input);
    switch (parsed.action) {
      case "start": {
        const command = parsed.command;
        if (typeof command !== "string" || command.trim().length === 0) {
          return asText2(
            "Starting a job needs a non-empty 'command' to run.",
            true
          );
        }
        return startJob(ctx, command);
      }
      case "list":
        return listJobs();
      case "poll": {
        const id = parsed.id;
        if (typeof id !== "string" || id.trim().length === 0) {
          return asText2("Polling a job needs the 'id' it was given at start.", true);
        }
        return pollJob(ctx, id);
      }
      case "stop": {
        const id = parsed.id;
        if (typeof id !== "string" || id.trim().length === 0) {
          return asText2("Stopping a job needs the 'id' it was given at start.", true);
        }
        return stopJob(id);
      }
      default:
        return asText2("Unsupported action.", true);
    }
  }
});

// src/capabilities/planning/todo.ts
var VALID_STATUSES = /* @__PURE__ */ new Set([
  "pending",
  "active",
  "done"
]);
var TodoStore = class {
  lists = /* @__PURE__ */ new Map();
  /** Return the list recorded for `ctx`, or an empty array if none was set yet. */
  read(ctx) {
    return this.lists.get(ctx) ?? [];
  }
  /** Replace the entire list recorded for `ctx` with `items`. */
  replace(ctx, items) {
    this.lists.set(ctx, items);
  }
  /** Discard every stored list. Intended for test isolation only. */
  reset() {
    this.lists.clear();
  }
};
var todoStore = new TodoStore();
function reset() {
  todoStore.reset();
}
function failure4(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function asBag(input) {
  if (input !== null && typeof input === "object" && !Array.isArray(input)) {
    return input;
  }
  return {};
}
function parseItem(raw, position) {
  const where = `item ${position + 1}`;
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
    return { ok: false, reason: `${where} must be an object describing a task.` };
  }
  const bag = raw;
  const id = bag.id;
  if (typeof id !== "string" || id.trim().length === 0) {
    return { ok: false, reason: `${where} needs a non-empty string \`id\`.` };
  }
  const text = bag.text;
  if (typeof text !== "string" || text.trim().length === 0) {
    return { ok: false, reason: `${where} needs a non-empty string \`text\`.` };
  }
  const status = bag.status;
  if (typeof status !== "string" || !VALID_STATUSES.has(status)) {
    return {
      ok: false,
      reason: `${where} has an unrecognised \`status\`; use one of pending, active, or done.`
    };
  }
  return { ok: true, item: { id, text, status } };
}
function parseItems(raw) {
  if (!Array.isArray(raw)) {
    return { ok: false, reason: "`items` must be an array of task objects (send [] to clear the list)." };
  }
  const items = [];
  const seen = /* @__PURE__ */ new Set();
  for (let i = 0; i < raw.length; i += 1) {
    const parsed = parseItem(raw[i], i);
    if (!parsed.ok) return parsed;
    if (seen.has(parsed.item.id)) {
      return { ok: false, reason: `Duplicate id "${parsed.item.id}"; each task id must be unique.` };
    }
    seen.add(parsed.item.id);
    items.push(parsed.item);
  }
  return { ok: true, items };
}
function renderList(items) {
  if (items.length === 0) {
    return "The task list is currently empty.";
  }
  const glyph = {
    pending: "[ ]",
    active: "[~]",
    done: "[x]"
  };
  const lines = items.map(
    (item) => `${glyph[item.status]} (${item.id}) ${item.text}`
  );
  const counts = summarise(items);
  return `${counts}
${lines.join("\n")}`;
}
function summarise(items) {
  let pending = 0;
  let active = 0;
  let done = 0;
  for (const item of items) {
    if (item.status === "pending") pending += 1;
    else if (item.status === "active") active += 1;
    else done += 1;
  }
  return `${items.length} task${items.length === 1 ? "" : "s"}: ${active} active, ${pending} pending, ${done} done.`;
}
var setParameters = {
  type: "object",
  additionalProperties: false,
  required: ["items"],
  properties: {
    items: {
      type: "array",
      description: "The complete task list to store, replacing whatever was tracked before. Send an empty array to clear the list entirely.",
      items: {
        type: "object",
        additionalProperties: false,
        required: ["id", "text", "status"],
        properties: {
          id: {
            type: "string",
            description: "Stable identifier for the task; must be unique within the list."
          },
          text: {
            type: "string",
            description: "Short description of what the task involves."
          },
          status: {
            type: "string",
            enum: ["pending", "active", "done"],
            description: "Lifecycle stage: pending (not started), active (in progress), or done (finished)."
          }
        }
      }
    }
  }
};
var SET_DESCRIPTION = [
  "Record the full task checklist for the current session, overwriting any list",
  "stored earlier. Pass the entire desired list every time \u2014 this is a replace,",
  "not a merge \u2014 so keep finished tasks in the array (with status done) until you",
  "no longer want them shown. Use this to plan multi-step work and to mark progress",
  "as each step starts and completes."
].join(" ");
var todoSetTool = defineTool({
  name: "todo_set",
  description: SET_DESCRIPTION,
  parameters: setParameters,
  async run(input, ctx) {
    const bag = asBag(input);
    if (!("items" in bag)) {
      return failure4("An `items` array is required; send [] to empty the list.");
    }
    const parsed = parseItems(bag.items);
    if (!parsed.ok) return failure4(parsed.reason);
    todoStore.replace(ctx, parsed.items);
    const body = `Task list updated. ${renderList(parsed.items)}`;
    return { content: [{ kind: "text", text: body }] };
  }
});
var READ_DESCRIPTION = [
  "Show the task checklist currently stored for this session, including each",
  "task's id, description, and status. Use it to remind yourself of the plan and",
  "of what is still outstanding before deciding what to do next. This is read-only",
  "and changes nothing."
].join(" ");
var todoReadTool = defineTool({
  name: "todo_read",
  description: READ_DESCRIPTION,
  parameters: {
    type: "object",
    additionalProperties: false,
    properties: {}
  },
  async run(_input, ctx) {
    const items = todoStore.read(ctx);
    return { content: [{ kind: "text", text: renderList(items) }] };
  }
});

// src/capabilities/web/websearch.ts
var DEFAULT_RESULTS = 5;
var MAX_RESULTS = 25;
var REQUEST_TIMEOUT_MS = 1e4;
var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/";
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";
var DESCRIPTION5 = [
  "Search the public web for a query and return the leading results as a list of",
  "{ title, url, snippet } entries. Useful for finding current information,",
  "documentation, or sources that are not already in the workspace. Use",
  "`maxResults` to ask for more or fewer hits; the count is capped to keep the",
  "response compact. If the network is unreachable the tool reports a failure",
  "instead of throwing."
].join(" ");
function failure5(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function resolveMaxResults(value) {
  if (value === void 0 || value === null) {
    return { ok: true, value: DEFAULT_RESULTS };
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    return { ok: false, reason: "`maxResults` must be a number when provided." };
  }
  if (!Number.isInteger(value)) {
    return { ok: false, reason: `\`maxResults\` must be a whole number, not ${value}.` };
  }
  if (value < 1) {
    return { ok: false, reason: `\`maxResults\` must be at least 1; got ${value}.` };
  }
  return { ok: true, value: Math.min(value, MAX_RESULTS) };
}
function toPlainText(html) {
  const withoutTags = html.replace(/<[^>]*>/g, "");
  const decoded = withoutTags.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ");
  return decoded.replace(/\s+/g, " ").trim();
}
function unwrapTarget(href) {
  let candidate = href;
  const redirectMatch = /[?&]uddg=([^&]+)/.exec(candidate);
  if (redirectMatch) {
    try {
      candidate = decodeURIComponent(redirectMatch[1]);
    } catch {
    }
  }
  if (candidate.startsWith("//")) {
    candidate = `https:${candidate}`;
  }
  return candidate;
}
function extractHits(body, limit) {
  const hits = [];
  const seen = /* @__PURE__ */ new Set();
  const titleAnchor = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
  const snippetBlock = /\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/[a-z]+>/i;
  let match;
  while ((match = titleAnchor.exec(body)) !== null) {
    if (hits.length >= limit) break;
    const url = unwrapTarget(match[1]);
    const title = toPlainText(match[2]);
    if (url.length === 0 || title.length === 0) continue;
    if (seen.has(url)) continue;
    const tail = body.slice(titleAnchor.lastIndex);
    const snippetMatch = snippetBlock.exec(tail);
    const snippet = snippetMatch ? toPlainText(snippetMatch[1]) : "";
    seen.add(url);
    hits.push({ title, url, snippet });
  }
  return hits;
}
function renderHits(query, hits) {
  const lines = [
    `${hits.length} result${hits.length === 1 ? "" : "s"} for "${query}":`,
    ""
  ];
  hits.forEach((hit, index) => {
    lines.push(`${index + 1}. ${hit.title}`);
    lines.push(`   ${hit.url}`);
    if (hit.snippet.length > 0) {
      lines.push(`   ${hit.snippet}`);
    }
    lines.push("");
  });
  return lines.join("\n").trimEnd();
}
var webSearchTool = defineTool({
  name: "websearch",
  description: DESCRIPTION5,
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "The words to search the web for."
      },
      maxResults: {
        type: "integer",
        minimum: 1,
        maximum: MAX_RESULTS,
        description: `How many hits to return at most. Defaults to ${DEFAULT_RESULTS}; values above ${MAX_RESULTS} are pulled down to that ceiling.`
      }
    },
    required: ["query"],
    additionalProperties: false
  },
  async run(input, ctx) {
    const raw = input;
    const bag = raw !== null && typeof raw === "object" ? raw : {};
    const rawQuery = bag.query;
    if (typeof rawQuery !== "string" || rawQuery.trim().length === 0) {
      return failure5("A non-empty `query` string is required to run a web search.");
    }
    const query = rawQuery.trim();
    const limitParse = resolveMaxResults(bag.maxResults);
    if (!limitParse.ok) return failure5(limitParse.reason);
    const limit = limitParse.value;
    const timeout = new AbortController();
    const onOuterAbort = () => timeout.abort();
    if (ctx.signal.aborted) {
      return failure5("The search was cancelled before the request went out.");
    }
    ctx.signal.addEventListener("abort", onOuterAbort, { once: true });
    const timer = setTimeout(() => timeout.abort(), REQUEST_TIMEOUT_MS);
    let body;
    try {
      const form = new URLSearchParams({ q: query });
      const response = await fetch(SEARCH_ENDPOINT, {
        method: "POST",
        signal: timeout.signal,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "User-Agent": USER_AGENT,
          Accept: "text/html"
        },
        body: form.toString()
      });
      if (!response.ok) {
        return failure5(
          `The search service answered with status ${response.status} (${response.statusText || "no reason given"}).`
        );
      }
      body = await response.text();
    } catch (err) {
      if (ctx.signal.aborted) {
        return failure5("The search was cancelled before it finished.");
      }
      if (timeout.signal.aborted) {
        return failure5(
          `The search timed out after ${REQUEST_TIMEOUT_MS / 1e3}s with no response.`
        );
      }
      const detail = err instanceof Error ? err.message : String(err);
      return failure5(`The web search could not reach the network: ${detail}`);
    } finally {
      clearTimeout(timer);
      ctx.signal.removeEventListener("abort", onOuterAbort);
    }
    const hits = extractHits(body, limit);
    if (hits.length === 0) {
      return failure5(
        `The search for "${query}" came back with no results that could be read.`
      );
    }
    const rendered = renderHits(query, hits);
    const clamped = clamp(rendered, {
      kind: ctx.budget.kind,
      maxBytes: ctx.budget.maxBytes,
      notice: (omitted) => `
[websearch: ${omitted} byte${omitted === 1 ? "" : "s"} of results were left out to stay within the response budget]
`
    });
    return {
      content: [
        { kind: "text", text: clamped },
        { kind: "json", value: hits }
      ]
    };
  }
});

// src/capabilities/web/webfetch.ts
var ABSOLUTE_BODY_CEILING = 5 * 1024 * 1024;
var DEFAULT_BODY_CEILING = 1024 * 1024;
var REQUEST_TIMEOUT_MS2 = 3e4;
var DESCRIPTION6 = [
  "Download a single web page over HTTP or HTTPS and return its text in a",
  "stripped-down, reading-friendly form. The page's HTML is reduced to plain",
  "text: headings, paragraphs, list items and links are kept in a simple layout,",
  "while scripts, styles, markup tags and boilerplate are removed. Supply an",
  "absolute `url`; optionally cap the amount of page data considered with",
  "`maxBytes`. The reply is always trimmed to fit the model's context, so very",
  "large pages come back partial with a marker showing where text was cut."
].join(" ");
function failure6(message) {
  return { content: [{ kind: "text", text: message }], isError: true };
}
function readByteCap(value) {
  if (value === void 0 || value === null) {
    return { ok: true, value: void 0 };
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    return { ok: false, reason: "`maxBytes` must be a number when you include it." };
  }
  if (!Number.isInteger(value)) {
    return { ok: false, reason: `\`maxBytes\` must be a whole number, not ${value}.` };
  }
  if (value < 1) {
    return { ok: false, reason: `\`maxBytes\` must be at least 1; got ${value}.` };
  }
  return { ok: true, value };
}
function parseTarget(raw) {
  let url;
  try {
    url = new URL(raw);
  } catch {
    return {
      ok: false,
      reason: `That does not look like a valid absolute URL: ${raw}`
    };
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") {
    return {
      ok: false,
      reason: `Only http and https addresses are supported; received "${url.protocol}".`
    };
  }
  return { ok: true, url };
}
var NAMED_ENTITIES = {
  amp: "&",
  lt: "<",
  gt: ">",
  quot: '"',
  apos: "'",
  nbsp: " ",
  hellip: "\u2026",
  mdash: "\u2014",
  ndash: "\u2013",
  copy: "\xA9",
  reg: "\xAE",
  trade: "\u2122"
};
function decodeEntities(text) {
  return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
    if (body[0] === "#") {
      const isHex = body[1] === "x" || body[1] === "X";
      const digits = isHex ? body.slice(2) : body.slice(1);
      const code = Number.parseInt(digits, isHex ? 16 : 10);
      if (!Number.isFinite(code) || code < 0 || code > 1114111) return whole;
      try {
        return String.fromCodePoint(code);
      } catch {
        return whole;
      }
    }
    const named = NAMED_ENTITIES[body.toLowerCase()];
    return named ?? whole;
  });
}
function dropRegion(html, tag) {
  const pattern = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}\\s*>`, "gi");
  return html.replace(pattern, " ");
}
function htmlToText(html) {
  let s = html;
  s = s.replace(/<!--[\s\S]*?-->/g, " ");
  for (const tag of ["script", "style", "noscript", "template", "head", "svg", "iframe"]) {
    s = dropRegion(s, tag);
  }
  s = s.replace(
    /<a\b[^>]*\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s">]+))[^>]*>([\s\S]*?)<\/a\s*>/gi,
    (_whole, _q, dq, sq, bare, label) => {
      const href = (dq ?? sq ?? bare ?? "").trim();
      const text = label.trim();
      if (!href) return text;
      if (!text) return href;
      return `${text} (${href})`;
    }
  );
  s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1\s*>/gi, (_whole, level, inner) => {
    const hashes = "#".repeat(Number.parseInt(level, 10));
    return `

${hashes} ${inner.trim()}

`;
  });
  s = s.replace(/<li\b[^>]*>/gi, "\n- ");
  s = s.replace(/<br\s*\/?>/gi, "\n");
  s = s.replace(
    /<\/?(p|div|section|article|header|footer|main|ul|ol|table|tr|h[1-6]|blockquote|pre)\b[^>]*>/gi,
    "\n\n"
  );
  s = s.replace(/<[^>]+>/g, " ");
  s = decodeEntities(s);
  const lines = s.split("\n").map((line) => line.replace(/[ \t\f\v ]+/g, " ").trim());
  const out = [];
  let blankRun = 0;
  for (const line of lines) {
    if (line === "") {
      blankRun += 1;
      if (blankRun <= 1) out.push("");
    } else {
      blankRun = 0;
      out.push(line);
    }
  }
  return out.join("\n").trim();
}
var webFetchTool = defineTool({
  name: "webfetch",
  description: DESCRIPTION6,
  parameters: {
    type: "object",
    properties: {
      url: {
        type: "string",
        description: "Absolute http or https address of the page to download."
      },
      maxBytes: {
        type: "integer",
        minimum: 1,
        description: "Optional ceiling on how many bytes of the page body to process before conversion."
      }
    },
    required: ["url"],
    additionalProperties: false
  },
  async run(input, ctx) {
    const raw = input;
    const bag = raw !== null && typeof raw === "object" ? raw : {};
    const rawUrl = bag.url;
    if (typeof rawUrl !== "string" || rawUrl.trim().length === 0) {
      return failure6("A non-empty `url` string is required to fetch a page.");
    }
    const target = parseTarget(rawUrl.trim());
    if (!target.ok) return failure6(target.reason);
    const capParse = readByteCap(bag.maxBytes);
    if (!capParse.ok) return failure6(capParse.reason);
    const requested = capParse.value ?? DEFAULT_BODY_CEILING;
    const bodyCeiling = Math.min(requested, ABSOLUTE_BODY_CEILING);
    const timeout = new AbortController();
    const timer = setTimeout(() => {
      timeout.abort();
    }, REQUEST_TIMEOUT_MS2);
    const onOuterAbort = () => {
      timeout.abort();
    };
    ctx.signal.addEventListener("abort", onOuterAbort, { once: true });
    let response;
    try {
      response = await fetch(target.url.toString(), {
        method: "GET",
        redirect: "follow",
        signal: timeout.signal,
        headers: {
          // A neutral, honest accept header; many servers vary on it.
          accept: "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8"
        }
      });
    } catch (err) {
      clearTimeout(timer);
      ctx.signal.removeEventListener("abort", onOuterAbort);
      if (ctx.signal.aborted) {
        return failure6(`The fetch of ${target.url.toString()} was cancelled before it completed.`);
      }
      const detail = err instanceof Error ? err.message : String(err);
      return failure6(`Could not reach ${target.url.toString()}: ${detail}`);
    }
    if (!response.ok) {
      clearTimeout(timer);
      ctx.signal.removeEventListener("abort", onOuterAbort);
      return failure6(
        `The server answered ${response.status} (${response.statusText || "no status text"}) for ${target.url.toString()}.`
      );
    }
    let body;
    try {
      body = await response.text();
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      return failure6(`Downloaded ${target.url.toString()} but its body could not be decoded: ${detail}`);
    } finally {
      clearTimeout(timer);
      ctx.signal.removeEventListener("abort", onOuterAbort);
    }
    let rawTrimmed = false;
    if (Buffer.byteLength(body, "utf8") > bodyCeiling) {
      const clipped = clamp(body, { kind: "head", maxBytes: bodyCeiling });
      body = clipped;
      rawTrimmed = true;
    }
    const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
    const looksHtml = contentType.includes("html") || contentType.includes("xml") || contentType === "" && /<\s*\/?[a-z][\s\S]*>/i.test(body);
    const rendered = looksHtml ? htmlToText(body) : body.trim();
    if (rendered.length === 0) {
      return {
        content: [
          {
            kind: "text",
            text: `Fetched ${target.url.toString()} successfully, but no readable text remained after stripping the page.`
          }
        ]
      };
    }
    const clamped = clamp(rendered, {
      kind: ctx.budget.kind,
      maxBytes: ctx.budget.maxBytes,
      notice: (omitted) => `
[webfetch: dropped ${omitted} byte${omitted === 1 ? "" : "s"} of page text to stay inside the response budget]
`
    });
    const budgetTrimmed = clamped !== rendered;
    const headerParts = [];
    headerParts.push(`Fetched ${target.url.toString()}.`);
    if (contentType) {
      headerParts.push(`Content type: ${contentType}.`);
    }
    if (rawTrimmed) {
      headerParts.push(
        "The downloaded page exceeded the byte ceiling, so only its opening portion was processed."
      );
    }
    if (budgetTrimmed) {
      headerParts.push(
        "The rendered text was further shortened to fit the response budget (see the inline marker)."
      );
    }
    const documentBody = `${headerParts.join(" ")}

${clamped}`;
    return { content: [{ kind: "text", text: documentBody }] };
  }
});

// src/capabilities/registry.ts
var READ_ONLY_NAMES = [
  "read",
  "ls",
  "grep",
  "find",
  "websearch",
  "webfetch"
];
var MUTATING_NAMES = [
  "write",
  "edit",
  "bash",
  "todo_set",
  "todo_read",
  "process"
];
function allTools() {
  return [
    // read-only
    readTool,
    lsTool,
    grepTool,
    findTool,
    webSearchTool,
    webFetchTool,
    // mutating / stateful
    writeTool,
    editTool,
    bashTool,
    todoSetTool,
    todoReadTool,
    processTool
  ];
}
function builtinRegistry() {
  const registry = new ToolRegistry();
  registry.registerAll(allTools());
  registry.collection("read-only", READ_ONLY_NAMES);
  registry.collection("coding", [...READ_ONLY_NAMES, ...MUTATING_NAMES]);
  registry.collection("all", registry.names());
  return registry;
}
function toolBox(collection, cwd) {
  const root = cwd ?? process.cwd();
  const registry = builtinRegistry();
  const ctxFactory = () => makeNodeContext(root);
  return registry.toToolBox(ctxFactory, collection);
}
export {
  TodoStore,
  ToolRegistry,
  UnknownToolError,
  bashTool,
  builtinRegistry,
  clamp,
  defineTool,
  editTool,
  findTool,
  grepTool,
  lsTool,
  makeNodeContext,
  nodeFs,
  nodeShell,
  processTool,
  readTool,
  resetProcessTable,
  reset as resetTodos,
  todoReadTool,
  todoSetTool,
  todoStore,
  toolBox,
  webFetchTool,
  webSearchTool,
  writeTool
};
