@themoltnet/pi-extension 0.28.0 → 0.28.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.
package/README.md CHANGED
@@ -6,7 +6,8 @@ agent's MoltNet identity fully available inside the sandbox.
6
6
  ## How it works
7
7
 
8
8
  Every pi session boots a lightweight Alpine Linux VM from a cached snapshot.
9
- All file system and shell tools (read/write/edit/bash) execute inside the VM.
9
+ All file system, search, and shell tools (read/write/edit/bash/ls/find/grep)
10
+ execute inside the VM.
10
11
  MoltNet API tools (diary entries, pack ops, reflection) run on the host via
11
12
  the SDK and communicate outbound over HTTP.
12
13
 
@@ -24,7 +25,8 @@ pi + extension $MOLTNET_GUEST_WORKSPACE
24
25
  │ (via gitconfig ssh/allowed_signers
25
26
  │ in VM) /home/agent/.pi/agent/auth.json (pi OAuth)
26
27
 
27
- └─ read/write/edit/bash ─▶ vm.exec() / vm.fs.*
28
+ └─ read/write/edit/bash/ls/find/grep
29
+ ─▶ vm.exec() / vm.fs.*
28
30
  (redirected to VM)
29
31
  ```
30
32
 
@@ -104,6 +106,7 @@ tools can use structured in-process calls rather than shell round-trips.
104
106
  | Tool | Runs in | Mechanism |
105
107
  | ----------------------------------- | ------- | -------------------------------------------------------------------------------- |
106
108
  | `read`, `write`, `edit` | VM | Gondolin VFS — agent's FS view is `$MOLTNET_GUEST_WORKSPACE` |
109
+ | `ls`, `find`, `grep` | VM | Gondolin VFS — search/listing observes the sandboxed workspace |
107
110
  | `bash` | VM | `vm.exec()` — shell runs in the isolated guest |
108
111
  | `user_bash` (human `/bash` command) | VM | Same as agent bash |
109
112
  | `moltnet_pack_get` | Host | TypeScript SDK (`@themoltnet/sdk`) authenticated via injected `moltnet.json` |
package/dist/index.d.ts CHANGED
@@ -181,7 +181,7 @@ export declare interface CreateSubagentToolArgs {
181
181
  agentName: string;
182
182
  /**
183
183
  * Custom tools every subagent inherits (Gondolin-routed
184
- * Read/Write/Edit/Bash + moltnet_* tools, etc). MUST NOT include
184
+ * built-ins + moltnet_* tools, etc). MUST NOT include
185
185
  * the parent's submit-output tool, the parent's `subagent` tool,
186
186
  * or any other parent-only artefact — the caller is responsible
187
187
  * for filtering. The subagent appends its own submit tool.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
4
  import path, { join, relative, sep } from "node:path";
5
- import { DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
5
+ import { DEFAULT_MAX_BYTES, DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createFindTool, createFindToolDefinition, createGrepTool, createGrepToolDefinition, createLsTool, createLsToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { Readable } from "node:stream";
8
8
  import crypto, { createHash as createHash$1 } from "crypto";
@@ -18559,11 +18559,13 @@ function rewriteMoltnetJsonPaths(moltnetJson, vmAgentDir, vmSshDir, githubAppPem
18559
18559
  //#region src/tool-operations.ts
18560
18560
  /**
18561
18561
  * Gondolin tool operations: redirect pi's built-in tool operations
18562
- * (read, write, edit, bash) to execute inside the VM.
18562
+ * (read, write, edit, bash, ls, find, grep) to execute inside the VM.
18563
18563
  *
18564
18564
  * Follows the same pattern as upstream pi-gondolin.ts — pi's tool factories
18565
18565
  * accept an `operations` object that provides the underlying I/O.
18566
18566
  */
18567
+ var DEFAULT_GREP_LIMIT = 100;
18568
+ var GREP_MAX_FILE_SIZE = "2M";
18567
18569
  function shQuote(s) {
18568
18570
  return "'" + s.replace(/'/g, "'\\''") + "'";
18569
18571
  }
@@ -18573,6 +18575,18 @@ function normalizeGuestPath(p) {
18573
18575
  function isSameOrInsidePosixPath(candidate, root) {
18574
18576
  return candidate === root || candidate.startsWith(`${root}/`);
18575
18577
  }
18578
+ function resolveLocalPath(localCwd, inputPath) {
18579
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(localCwd, inputPath);
18580
+ }
18581
+ function toHostToolPath(localCwd, guestWorkspace, guestPath) {
18582
+ const normalizedGuestWorkspace = normalizeGuestPath(guestWorkspace);
18583
+ const normalizedGuestPath = normalizeGuestPath(guestPath);
18584
+ if (isSameOrInsidePosixPath(normalizedGuestPath, normalizedGuestWorkspace)) {
18585
+ const rel = path.posix.relative(normalizedGuestWorkspace, normalizedGuestPath);
18586
+ return rel ? path.join(localCwd, ...rel.split("/")) : localCwd;
18587
+ }
18588
+ return normalizedGuestPath;
18589
+ }
18576
18590
  /**
18577
18591
  * Map a host-side absolute path to a guest-side workspace path.
18578
18592
  * Throws if the path escapes the workspace.
@@ -18592,17 +18606,10 @@ function toGuestPath(localCwd, localPath, guestWorkspace) {
18592
18606
  function createGondolinReadOps(vm, localCwd, guestWorkspace) {
18593
18607
  return {
18594
18608
  readFile: async (p) => {
18595
- const r = await vm.exec(["/bin/cat", toGuestPath(localCwd, p, guestWorkspace)]);
18596
- if (!r.ok) throw new Error(`cat failed (${r.exitCode}): ${r.stderr}`);
18597
- return r.stdoutBuffer;
18598
- },
18599
- access: async (p) => {
18600
- if (!(await vm.exec([
18601
- "/bin/sh",
18602
- "-lc",
18603
- `test -r ${shQuote(toGuestPath(localCwd, p, guestWorkspace))}`
18604
- ])).ok) throw new Error(`not readable: ${p}`);
18609
+ const content = await vm.fs.readFile(toGuestPath(localCwd, p, guestWorkspace));
18610
+ return typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
18605
18611
  },
18612
+ access: async (p) => vm.fs.access(toGuestPath(localCwd, p, guestWorkspace)),
18606
18613
  detectImageMimeType: async (p) => {
18607
18614
  try {
18608
18615
  const r = await vm.exec([
@@ -18660,6 +18667,253 @@ function createGondolinEditOps(vm, localCwd, guestWorkspace) {
18660
18667
  writeFile: w.writeFile
18661
18668
  };
18662
18669
  }
18670
+ function createGondolinLsOps(vm, localCwd, guestWorkspace) {
18671
+ return {
18672
+ exists: async (p) => {
18673
+ try {
18674
+ await vm.fs.access(toGuestPath(localCwd, p, guestWorkspace));
18675
+ return true;
18676
+ } catch {
18677
+ return false;
18678
+ }
18679
+ },
18680
+ stat: async (p) => vm.fs.stat(toGuestPath(localCwd, p, guestWorkspace)),
18681
+ readdir: async (p) => vm.fs.listDir(toGuestPath(localCwd, p, guestWorkspace))
18682
+ };
18683
+ }
18684
+ async function walkGuestFiles(vm, root, visit, signal) {
18685
+ if (signal?.aborted) throw new Error("Operation aborted");
18686
+ if (!(await vm.fs.stat(root, { signal })).isDirectory()) return visit(root, path.posix.basename(root));
18687
+ const walkDirectory = async (dir, relativeDir) => {
18688
+ if (signal?.aborted) throw new Error("Operation aborted");
18689
+ const entries = await vm.fs.listDir(dir, { signal });
18690
+ for (const entry of entries) {
18691
+ if (entry === ".git" || entry === "node_modules") continue;
18692
+ const guestPath = path.posix.join(dir, entry);
18693
+ const relativePath = relativeDir ? path.posix.join(relativeDir, entry) : entry;
18694
+ let entryStat;
18695
+ try {
18696
+ entryStat = await vm.fs.stat(guestPath, { signal });
18697
+ } catch {
18698
+ continue;
18699
+ }
18700
+ if (entryStat.isDirectory()) {
18701
+ if (!await walkDirectory(guestPath, relativePath)) return false;
18702
+ } else if (!await visit(guestPath, relativePath)) return false;
18703
+ }
18704
+ return true;
18705
+ };
18706
+ return walkDirectory(root, "");
18707
+ }
18708
+ function matchesGlob(relativePath, pattern) {
18709
+ return path.posix.matchesGlob(relativePath, pattern);
18710
+ }
18711
+ function matchesToolGlob(relativePath, pattern) {
18712
+ const normalizedPattern = normalizeGuestPath(pattern);
18713
+ if (normalizedPattern.includes("/")) return matchesGlob(relativePath, normalizedPattern) || matchesGlob(relativePath, `**/${normalizedPattern}`);
18714
+ return matchesGlob(path.posix.basename(relativePath), normalizedPattern);
18715
+ }
18716
+ function createGondolinFindOps(vm, localCwd, guestWorkspace) {
18717
+ return {
18718
+ exists: async (p) => {
18719
+ try {
18720
+ await vm.fs.access(toGuestPath(localCwd, p, guestWorkspace));
18721
+ return true;
18722
+ } catch {
18723
+ return false;
18724
+ }
18725
+ },
18726
+ glob: async (pattern, cwd, options) => {
18727
+ const root = toGuestPath(localCwd, cwd, guestWorkspace);
18728
+ const results = [];
18729
+ await walkGuestFiles(vm, root, (guestPath, relativePath) => {
18730
+ if (results.length >= options.limit) return Promise.resolve(false);
18731
+ if (options.ignore.some((ignore) => matchesToolGlob(relativePath, ignore))) return Promise.resolve(true);
18732
+ if (matchesToolGlob(relativePath, pattern)) results.push(toHostToolPath(localCwd, guestWorkspace, guestPath));
18733
+ return Promise.resolve(results.length < options.limit);
18734
+ });
18735
+ return results;
18736
+ }
18737
+ };
18738
+ }
18739
+ function appendGrepBlock(params) {
18740
+ let linesTruncated = false;
18741
+ const start = params.contextLines > 0 ? Math.max(0, params.lineIndex - params.contextLines) : params.lineIndex;
18742
+ const end = params.contextLines > 0 ? Math.min(params.lines.length - 1, params.lineIndex + params.contextLines) : params.lineIndex;
18743
+ for (let index = start; index <= end; index++) {
18744
+ const { text, wasTruncated } = truncateLine((params.lines[index] ?? "").replace(/\r/g, ""));
18745
+ if (wasTruncated) linesTruncated = true;
18746
+ const separator = index === params.lineIndex ? ":" : "-";
18747
+ params.outputLines.push(`${params.relativePath}${separator}${index + 1}${separator} ${text}`);
18748
+ }
18749
+ return linesTruncated;
18750
+ }
18751
+ function parseRgJsonLines(params) {
18752
+ const parts = `${params.carry}${params.chunk}`.split("\n");
18753
+ const carry = parts.pop() ?? "";
18754
+ let limitReached = false;
18755
+ for (const line of parts) {
18756
+ if (!line.trim() || params.matches.length >= params.effectiveLimit) continue;
18757
+ let event;
18758
+ try {
18759
+ event = JSON.parse(line);
18760
+ } catch {
18761
+ continue;
18762
+ }
18763
+ const candidate = event;
18764
+ if (candidate.type !== "match") continue;
18765
+ const guestPath = candidate.data?.path?.text;
18766
+ const lineNumber = candidate.data?.line_number;
18767
+ if (!guestPath || typeof lineNumber !== "number") continue;
18768
+ params.matches.push({
18769
+ guestPath,
18770
+ lineNumber,
18771
+ lineText: candidate.data?.lines?.text
18772
+ });
18773
+ if (params.matches.length >= params.effectiveLimit) {
18774
+ limitReached = true;
18775
+ break;
18776
+ }
18777
+ }
18778
+ return {
18779
+ carry,
18780
+ limitReached
18781
+ };
18782
+ }
18783
+ async function readGuestLines(vm, guestPath, signal) {
18784
+ try {
18785
+ return (await vm.fs.readFile(guestPath, {
18786
+ encoding: "utf8",
18787
+ signal
18788
+ })).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
18789
+ } catch {
18790
+ return [];
18791
+ }
18792
+ }
18793
+ async function executeGondolinGrep(vm, localCwd, guestWorkspace, params, signal) {
18794
+ const root = toGuestPath(localCwd, resolveLocalPath(localCwd, params.path ?? "."), guestWorkspace);
18795
+ let rootStat;
18796
+ try {
18797
+ rootStat = await vm.fs.stat(root, { signal });
18798
+ } catch {
18799
+ throw new Error(`Path not found: ${resolveLocalPath(localCwd, params.path ?? ".")}`);
18800
+ }
18801
+ const rootIsDirectory = rootStat.isDirectory();
18802
+ const contextLines = params.context && params.context > 0 ? params.context : 0;
18803
+ const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
18804
+ const args = [
18805
+ "--json",
18806
+ "--line-number",
18807
+ "--color=never",
18808
+ "--hidden",
18809
+ "--max-filesize",
18810
+ GREP_MAX_FILE_SIZE
18811
+ ];
18812
+ if (params.ignoreCase) args.push("--ignore-case");
18813
+ if (params.literal) args.push("--fixed-strings");
18814
+ if (params.glob) args.push("--glob", params.glob);
18815
+ args.push("--", params.pattern, root);
18816
+ const outputLines = [];
18817
+ const details = {};
18818
+ const matches = [];
18819
+ let matchLimitReached = false;
18820
+ let linesTruncated = false;
18821
+ let stderr = "";
18822
+ let carry = "";
18823
+ const ac = new AbortController();
18824
+ const onAbort = () => ac.abort();
18825
+ signal?.addEventListener("abort", onAbort, { once: true });
18826
+ try {
18827
+ const proc = vm.exec(["/bin/rg", ...args], {
18828
+ signal: ac.signal,
18829
+ stdout: "pipe",
18830
+ stderr: "pipe"
18831
+ });
18832
+ for await (const chunk of proc.output()) {
18833
+ const text = typeof chunk.data === "string" ? chunk.data : Buffer.from(chunk.data).toString("utf8");
18834
+ if (chunk.stream === "stderr") {
18835
+ stderr += text;
18836
+ continue;
18837
+ }
18838
+ const parsed = parseRgJsonLines({
18839
+ chunk: text,
18840
+ carry,
18841
+ matches,
18842
+ effectiveLimit
18843
+ });
18844
+ carry = parsed.carry;
18845
+ if (parsed.limitReached) {
18846
+ matchLimitReached = true;
18847
+ ac.abort();
18848
+ break;
18849
+ }
18850
+ }
18851
+ const r = await proc;
18852
+ if (!signal?.aborted && !matchLimitReached && r.exitCode !== 0 && r.exitCode !== 1) throw new Error(stderr.trim() || `ripgrep exited with code ${r.exitCode}`);
18853
+ } catch (err) {
18854
+ if (signal?.aborted) throw new Error("Operation aborted");
18855
+ if (matchLimitReached) {} else throw err;
18856
+ } finally {
18857
+ signal?.removeEventListener("abort", onAbort);
18858
+ }
18859
+ if (matches.length === 0) return {
18860
+ content: [{
18861
+ type: "text",
18862
+ text: "No matches found"
18863
+ }],
18864
+ details: void 0
18865
+ };
18866
+ const fileCache = /* @__PURE__ */ new Map();
18867
+ for (const match of matches) {
18868
+ if (signal?.aborted) throw new Error("Operation aborted");
18869
+ const displayPath = rootIsDirectory ? path.posix.relative(root, match.guestPath) : path.posix.basename(match.guestPath);
18870
+ if (contextLines === 0 && match.lineText !== void 0) {
18871
+ const { text, wasTruncated } = truncateLine(match.lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, ""));
18872
+ if (wasTruncated) linesTruncated = true;
18873
+ outputLines.push(`${displayPath}:${match.lineNumber}: ${text}`);
18874
+ continue;
18875
+ }
18876
+ let lines = fileCache.get(match.guestPath);
18877
+ if (!lines) {
18878
+ lines = await readGuestLines(vm, match.guestPath, signal);
18879
+ fileCache.set(match.guestPath, lines);
18880
+ }
18881
+ if (lines.length === 0) {
18882
+ outputLines.push(`${displayPath}:${match.lineNumber}: (unable to read file)`);
18883
+ continue;
18884
+ }
18885
+ if (appendGrepBlock({
18886
+ outputLines,
18887
+ lines,
18888
+ relativePath: displayPath,
18889
+ lineIndex: match.lineNumber - 1,
18890
+ contextLines
18891
+ })) linesTruncated = true;
18892
+ }
18893
+ const truncation = truncateHead(outputLines.join("\n"), { maxLines: Number.MAX_SAFE_INTEGER });
18894
+ const notices = [];
18895
+ let output = truncation.content;
18896
+ if (matchLimitReached) {
18897
+ details.matchLimitReached = effectiveLimit;
18898
+ notices.push(`${effectiveLimit} matches limit reached`);
18899
+ }
18900
+ if (linesTruncated) {
18901
+ details.linesTruncated = true;
18902
+ notices.push("long lines truncated");
18903
+ }
18904
+ if (truncation.truncated) {
18905
+ details.truncation = truncation;
18906
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
18907
+ }
18908
+ if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
18909
+ return {
18910
+ content: [{
18911
+ type: "text",
18912
+ text: output
18913
+ }],
18914
+ details: Object.keys(details).length > 0 ? details : void 0
18915
+ };
18916
+ }
18663
18917
  function createGondolinBashOps(vm, localCwd, guestWorkspace) {
18664
18918
  return { exec: async (command, cwd, { onData, signal, timeout, env }) => {
18665
18919
  const guestCwd = toGuestPath(localCwd, cwd, guestWorkspace);
@@ -24292,7 +24546,7 @@ function subagentToolDescription() {
24292
24546
  "",
24293
24547
  "The subagent starts with no conversation history and only the `task` ",
24294
24548
  "string you provide as its instructions. It runs in the same VM with ",
24295
- "the same tools you have (Gondolin-routed Read/Write/Edit/Bash, ",
24549
+ "the same tools you have (Gondolin-routed built-ins, ",
24296
24550
  "moltnet_* tools), and is expected to call ",
24297
24551
  `\`${SUBAGENT_SUBMIT_TOOL_NAME}\` with a payload matching the named `,
24298
24552
  "contract before its session ends.",
@@ -24784,7 +25038,7 @@ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
24784
25038
  *
24785
25039
  * This is the pi-specific task executor. It owns:
24786
25040
  * - VM lifecycle (ensureSnapshot + resumeVm + close)
24787
- * - Gondolin-redirected tool wiring (read/write/edit/bash → VM)
25041
+ * - Gondolin-redirected tool wiring (read/write/edit/bash/ls/find/grep → VM)
24788
25042
  * - MoltNet custom tools (diary entries, pack render/judge, etc.)
24789
25043
  * - pi createAgentSession + event → TaskReporter bridge
24790
25044
  *
@@ -24795,6 +25049,25 @@ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
24795
25049
  * `AgentRuntime`.
24796
25050
  */
24797
25051
  var noopTurnEventHandler = () => {};
25052
+ function createGondolinToolDefinitions(config) {
25053
+ const { vm, mountPath, guestWorkspace } = config;
25054
+ const grepTool = createGrepToolDefinition(mountPath);
25055
+ return [
25056
+ createReadToolDefinition(mountPath, { operations: createGondolinReadOps(vm, mountPath, guestWorkspace) }),
25057
+ createWriteToolDefinition(mountPath, { operations: createGondolinWriteOps(vm, mountPath, guestWorkspace) }),
25058
+ createEditToolDefinition(mountPath, { operations: createGondolinEditOps(vm, mountPath, guestWorkspace) }),
25059
+ createBashToolDefinition(mountPath, { operations: createGondolinBashOps(vm, mountPath, guestWorkspace) }),
25060
+ createLsToolDefinition(mountPath, { operations: createGondolinLsOps(vm, mountPath, guestWorkspace) }),
25061
+ createFindToolDefinition(mountPath, { operations: createGondolinFindOps(vm, mountPath, guestWorkspace) }),
25062
+ {
25063
+ ...grepTool,
25064
+ async execute(...args) {
25065
+ const [_id, params, signal] = args;
25066
+ return executeGondolinGrep(vm, mountPath, guestWorkspace, params, signal);
25067
+ }
25068
+ }
25069
+ ];
25070
+ }
24798
25071
  /**
24799
25072
  * Factory that builds a pi-specific `executeTask` function suitable for
24800
25073
  * injection into `AgentRuntime`. The returned function caches the resolved
@@ -25058,12 +25331,11 @@ async function executePiTask(claimedTask, reporter, opts) {
25058
25331
  slugs: injectedContext.injected.map((r) => r.slug)
25059
25332
  });
25060
25333
  if (injectedContext.userInlineSuffix) taskPrompt = `${taskPrompt}\n\n---\n\n${injectedContext.userInlineSuffix}`;
25061
- const gondolinCustomTools = [
25062
- createReadToolDefinition(mountPath, { operations: createGondolinReadOps(managed.vm, mountPath, managed.guestWorkspace) }),
25063
- createWriteToolDefinition(mountPath, { operations: createGondolinWriteOps(managed.vm, mountPath, managed.guestWorkspace) }),
25064
- createEditToolDefinition(mountPath, { operations: createGondolinEditOps(managed.vm, mountPath, managed.guestWorkspace) }),
25065
- createBashToolDefinition(mountPath, { operations: createGondolinBashOps(managed.vm, mountPath, managed.guestWorkspace) })
25066
- ];
25334
+ const gondolinCustomTools = createGondolinToolDefinitions({
25335
+ vm: managed.vm,
25336
+ mountPath,
25337
+ guestWorkspace: managed.guestWorkspace
25338
+ });
25067
25339
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
25068
25340
  model: opts.model,
25069
25341
  input: task.input
@@ -25591,6 +25863,9 @@ function moltnetExtension(pi) {
25591
25863
  const localWrite = createWriteTool(localCwd);
25592
25864
  const localEdit = createEditTool(localCwd);
25593
25865
  const localBash = createBashTool(localCwd);
25866
+ const localLs = createLsTool(localCwd);
25867
+ const localFind = createFindTool(localCwd);
25868
+ const localGrep = createGrepTool(localCwd);
25594
25869
  const initialGuestWorkspace = path.resolve(localCwd);
25595
25870
  let vm = null;
25596
25871
  let guestWorkspace = initialGuestWorkspace;
@@ -25701,6 +25976,24 @@ function moltnetExtension(pi) {
25701
25976
  return createBashTool(localCwd, { operations: createGondolinBashOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
25702
25977
  }
25703
25978
  });
25979
+ pi.registerTool({
25980
+ ...localLs,
25981
+ async execute(id, params, signal, onUpdate, ctx) {
25982
+ return createLsTool(localCwd, { operations: createGondolinLsOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
25983
+ }
25984
+ });
25985
+ pi.registerTool({
25986
+ ...localFind,
25987
+ async execute(id, params, signal, onUpdate, ctx) {
25988
+ return createFindTool(localCwd, { operations: createGondolinFindOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
25989
+ }
25990
+ });
25991
+ pi.registerTool({
25992
+ ...localGrep,
25993
+ async execute(_id, params, signal, _onUpdate, ctx) {
25994
+ return executeGondolinGrep(await ensureVm(ctx), localCwd, guestWorkspace, params, signal);
25995
+ }
25996
+ });
25704
25997
  pi.on("user_bash", (_event, _ctx) => {
25705
25998
  if (!vm) return;
25706
25999
  return { operations: createGondolinBashOps(vm, localCwd, guestWorkspace) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "keywords": [