@better-fs-tools/pi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,15 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { TSchema } from "typebox";
3
3
  import type { JsonObject } from "@better-fs-tools/read";
4
- import type { ShellToolDeps } from "@better-fs-tools/shell";
5
- import type { BashSignature } from "@better-fs-tools/shell/signature";
4
+ import type { BashToolDefinition, BashToolDefinitionOptions } from "@better-fs-tools/shell/signature";
6
5
  import type { PiContentPart } from "./parts.ts";
7
- /** Options for createPiBashTool. Commands always run in ctx.cwd. */
8
- export interface CreatePiBashToolOptions extends Omit<ShellToolDeps<ExtensionContext>, "runner" | "cwd" | "env"> {
9
- /** Default a nodeCommandRunner at ctx.cwd. */
10
- readonly runner?: ShellToolDeps<ExtensionContext>["runner"];
11
- /** Default process.env with defaultShellEnv over it. */
12
- readonly env?: ShellToolDeps<ExtensionContext>["env"];
13
- /**
14
- * Default Pi's own bash input, { command, timeout } with the timeout in
15
- * seconds, so Pi's bash renderer and prompt fit. Its description names the
16
- * configured timeouts, output limits, and runner id.
17
- */
18
- readonly signature?: BashSignature;
19
- readonly promptSnippet?: string;
20
- readonly promptGuidelines?: readonly string[];
21
- }
6
+ import type { PiPromptOptions } from "./prompt.ts";
7
+ /**
8
+ * Options for createPiBashTool: the options of bashToolDefinition(), with the
9
+ * Pi extension context as host, plus the Pi prompt options. `runner` and `env`
10
+ * are required: piCwdCommandRunner() from `./node` runs commands in ctx.cwd.
11
+ */
12
+ export type PiBashToolOptions = BashToolDefinitionOptions<ExtensionContext> & PiPromptOptions;
22
13
  /** Pi's BashToolDetails shape, so Pi's bash renderer can show the saved output. */
23
14
  export interface PiBashDetails {
24
15
  fullOutputPath?: string;
@@ -26,8 +17,10 @@ export interface PiBashDetails {
26
17
  export interface PiBashToolResult {
27
18
  content: PiContentPart[];
28
19
  details: PiBashDetails | undefined;
20
+ /** true for a nonzero exit, a timeout, and an error. */
21
+ isError: boolean;
29
22
  }
30
- /** Assignable to ToolDefinition<TSchema, PiBashDetails | undefined> from Pi 0.84.2. */
23
+ /** Assignable to ToolDefinition<TSchema, PiBashDetails | undefined> from Pi 1.x. */
31
24
  export interface PiBashTool {
32
25
  readonly name: string;
33
26
  readonly label: string;
@@ -39,10 +32,9 @@ export interface PiBashTool {
39
32
  execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiBashToolResult>;
40
33
  }
41
34
  /**
42
- * Creates a Pi bash tool that runs commands in ctx.cwd, also with your own
43
- * runner. Throws TypeError when options set fs, cwd, or allowedRoots, and
44
- * execute throws TypeError when ctx.cwd is not an absolute path. The Pi
45
- * extension does not register this tool, so Pi's own bash stays in place
46
- * unless you register it.
35
+ * Creates a Pi bash tool with the options and defaults of createBashTool().
36
+ * The Pi extension does not register this tool, so Pi's own bash stays in
37
+ * place unless you register it.
47
38
  */
48
- export declare function createPiBashTool(options?: CreatePiBashToolOptions): PiBashTool;
39
+ export declare function createPiBashTool(options: PiBashToolOptions): PiBashTool;
40
+ export declare function adaptPiBashTool(definition: BashToolDefinition<ExtensionContext>, prompt: PiPromptOptions): PiBashTool;
package/dist/bash-tool.js CHANGED
@@ -1,76 +1,41 @@
1
- import { isAbsolute } from "node:path";
2
1
  import { Type } from "typebox";
3
- import { nodeCommandRunner } from "@better-fs-tools/node";
4
- import { createBashTool, shellEnv } from "@better-fs-tools/shell";
5
- import { bashSignatureMessages, defaultBashSignature } from "@better-fs-tools/shell/signature";
2
+ import { bashToolDefinition } from "@better-fs-tools/shell/signature";
6
3
  import { toPiPart } from "./parts.js";
7
- import { checkPiContext, checkPiOptions } from "./roots.js";
8
- const PI_BUILTIN_BASH_PROMPT_SNIPPET = "Execute bash commands (ls, grep, find, etc.)";
9
- const MAX_CACHED_RUNNERS = 8;
4
+ import { checkPiOptions, piPrompt, splitPrompt } from "./prompt.js";
5
+ import { piBashArgs, setRenderArgs } from "./renderers.js";
10
6
  /**
11
- * Creates a Pi bash tool that runs commands in ctx.cwd, also with your own
12
- * runner. Throws TypeError when options set fs, cwd, or allowedRoots, and
13
- * execute throws TypeError when ctx.cwd is not an absolute path. The Pi
14
- * extension does not register this tool, so Pi's own bash stays in place
15
- * unless you register it.
7
+ * Creates a Pi bash tool with the options and defaults of createBashTool().
8
+ * The Pi extension does not register this tool, so Pi's own bash stays in
9
+ * place unless you register it.
16
10
  */
17
- export function createPiBashTool(options = {}) {
18
- checkPiOptions(options, "bash");
19
- const { signature: given, promptSnippet = PI_BUILTIN_BASH_PROMPT_SNIPPET, promptGuidelines = [], runner = recentRunnerPerCwd(), ...deps } = options;
20
- const runnerId = typeof runner === "function" ? undefined : runner.id;
21
- const signature = given ??
22
- defaultBashSignature({
23
- timeoutUnit: "s",
24
- cwd: false,
25
- ...(runnerId === undefined ? {} : { runner: runnerId }),
26
- ...(deps.limits === undefined ? {} : { limits: deps.limits }),
27
- });
28
- const bash = createBashTool({
29
- ...deps,
30
- runner,
31
- cwd: (call) => call.host.cwd,
32
- env: deps.env ?? shellEnv(() => process.env),
33
- messages: { ...bashSignatureMessages(signature), ...deps.messages },
34
- });
35
- return Object.freeze({
36
- name: signature.name,
37
- label: signature.name,
38
- description: signature.description,
39
- promptSnippet,
40
- promptGuidelines: [...promptGuidelines],
41
- parameters: Type.Unsafe(signature.schema),
11
+ export function createPiBashTool(options) {
12
+ checkPiOptions(options, "Pi bash tool");
13
+ const { prompt, rest } = splitPrompt(options);
14
+ return adaptPiBashTool(bashToolDefinition(rest), prompt);
15
+ }
16
+ export function adaptPiBashTool(definition, prompt) {
17
+ const { signature } = definition;
18
+ const tool = Object.freeze({
19
+ name: definition.name,
20
+ label: definition.name,
21
+ description: definition.description,
22
+ ...piPrompt(prompt, definition.description),
23
+ parameters: Type.Unsafe(definition.inputSchema),
42
24
  async execute(toolCallId, input, signal, _onUpdate, ctx) {
43
- checkPiContext(ctx, "bash");
44
- if (!isAbsolute(ctx.cwd))
45
- throw new TypeError("Pi bash execution requires an absolute ctx.cwd");
46
25
  const call = {
47
26
  ...(signal === undefined ? {} : { signal }),
48
27
  callId: toolCallId,
49
28
  host: ctx,
50
29
  };
51
- const result = await bash(signature.toInput(input), call);
30
+ const result = await definition.execute(input, call);
52
31
  const spill = result.output?.spill ?? null;
53
32
  return {
54
33
  content: result.content.map((part) => toPiPart(part, "bash")),
55
34
  details: spill === null ? undefined : { fullOutputPath: spill },
35
+ isError: result.status !== "ok",
56
36
  };
57
37
  },
58
38
  });
59
- }
60
- function recentRunnerPerCwd() {
61
- const cache = new Map();
62
- return (call) => {
63
- const cwd = call.host.cwd;
64
- const cached = cache.get(cwd);
65
- if (cached !== undefined) {
66
- cache.delete(cwd);
67
- cache.set(cwd, cached);
68
- return cached;
69
- }
70
- const made = nodeCommandRunner({ cwd });
71
- cache.set(cwd, made);
72
- if (cache.size > MAX_CACHED_RUNNERS)
73
- cache.delete(cache.keys().next().value);
74
- return made;
75
- };
39
+ setRenderArgs(tool, (args) => signature.toInput(args), piBashArgs);
40
+ return tool;
76
41
  }
package/dist/details.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ReadResult } from "@better-fs-tools/read";
1
+ import type { ReadFormatContext, ReadFormatter, ReadResult } from "@better-fs-tools/read";
2
2
  /** Pi's published TruncationResult. Fields and meanings are Pi's. */
3
3
  export interface PiTruncationResult {
4
4
  content: string;
@@ -24,3 +24,9 @@ export interface PiReadDetails {
24
24
  * total, because an approximate count is worse than none.
25
25
  */
26
26
  export declare function toPiReadDetails(result: ReadResult, view: string | null, maxViewBytes: number): PiReadDetails;
27
+ /**
28
+ * The formatted text of an ok read in "view" mode, for toPiReadDetails. null
29
+ * for another status, a formatter that returns parts, or a formatter that
30
+ * throws.
31
+ */
32
+ export declare function viewOf<THost>(formatter: ReadFormatter<THost>, result: ReadResult, ctx: ReadFormatContext<THost>): string | null;
package/dist/details.js CHANGED
@@ -28,3 +28,21 @@ export function toPiReadDetails(result, view, maxViewBytes) {
28
28
  },
29
29
  };
30
30
  }
31
+ /**
32
+ * The formatted text of an ok read in "view" mode, for toPiReadDetails. null
33
+ * for another status, a formatter that returns parts, or a formatter that
34
+ * throws.
35
+ */
36
+ export function viewOf(formatter, result, ctx) {
37
+ if (result.status !== "ok")
38
+ return null;
39
+ const { content: _content, ...outcome } = result;
40
+ try {
41
+ const view = formatter.format(outcome, ctx);
42
+ return typeof view === "string" ? view : null;
43
+ }
44
+ catch {
45
+ // The core already fell back and reported the error in "model" mode.
46
+ return null;
47
+ }
48
+ }
@@ -1,8 +1,20 @@
1
+ /**
2
+ * Pi package extension entry. Installing the package into a Pi project
3
+ * registers read, edit, and write, which replace Pi's own tools, and nothing
4
+ * else. It uses the recommended Pi configuration: a filesystem rooted at each
5
+ * call's ctx.cwd, Pi's own per-file queue as the locks, so Pi's edit and
6
+ * write wait for these tools, and nodeDigest(), which is faster on Node. Every
7
+ * other option has the shared default: no read store, and read returns images.
8
+ * A renderer resolver lets Pi's built-in read, edit, and write renderers draw
9
+ * them. Nothing is written outside the process: no settings file, no session file.
10
+ */
11
+ import type { ToolRendererResolver } from "@earendil-works/pi-coding-agent";
1
12
  import type { PiMutationTool } from "./mutation-tools.ts";
2
13
  import type { PiReadTool } from "./tool.ts";
3
14
  /** The part of Pi's ExtensionAPI this extension uses. Pi's full API satisfies it. */
4
15
  export interface PiExtensionApi {
5
16
  registerTool(tool: PiReadTool): void;
6
17
  registerTool(tool: PiMutationTool): void;
18
+ registerToolRenderer(resolver: ToolRendererResolver): void;
7
19
  }
8
20
  export default function fsToolsExtension(pi: PiExtensionApi): void;
package/dist/extension.js CHANGED
@@ -1,14 +1,16 @@
1
- /**
2
- * Pi package extension entry. Installing the package into a Pi project
3
- * registers read, edit, and write, as Pi's own tools are, and nothing else.
4
- * There is no read store, so edit and write do not check for a read first.
5
- * read returns images, as Pi's own read does.
6
- * Nothing is written outside the process: no settings file, no session file.
7
- */
1
+ import { nodeDigest } from "@better-fs-tools/node";
8
2
  import { createPiFsTools } from "./fs-tools.js";
3
+ import { piMutationQueueLocks } from "./locks.js";
4
+ import { piCwdFileSystem } from "./node.js";
5
+ import { piToolRenderers } from "./renderers.js";
9
6
  export default function fsToolsExtension(pi) {
10
- const tools = createPiFsTools();
7
+ const tools = createPiFsTools({
8
+ fs: piCwdFileSystem(),
9
+ locks: piMutationQueueLocks(),
10
+ digest: nodeDigest(),
11
+ });
11
12
  pi.registerTool(tools.read);
12
13
  pi.registerTool(tools.edit);
13
14
  pi.registerTool(tools.write);
15
+ pi.registerToolRenderer(piToolRenderers([tools.read, tools.edit, tools.write]));
14
16
  }
@@ -1,24 +1,48 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { Clock, Digest, ReadStateStore, ToolCallContext } from "@better-fs-tools/read";
3
- import type { FsToolsSharedKey, InvalidateOutcome, LockManager } from "@better-fs-tools/write";
4
- import type { CreatePiApplyPatchToolOptions, CreatePiEditToolOptions, CreatePiWriteToolOptions, PiMutationTool } from "./mutation-tools.ts";
5
- import type { PiRootOptions } from "./roots.ts";
6
- import type { CreatePiReadToolOptions, PiReadTool } from "./tool.ts";
7
- type BundleSharedKey = FsToolsSharedKey | keyof PiRootOptions;
8
- export interface CreatePiFsToolsOptions extends PiRootOptions {
9
- /** Default null: no read-before-write. A store, for example memoryStore(), turns it on. */
10
- readonly state?: ReadStateStore | null;
11
- /** Default nodeDigest(). */
12
- readonly digest?: Digest;
13
- /** Default memoryLocks(). */
14
- readonly locks?: LockManager;
15
- /** Default () => new Date(). */
16
- readonly clock?: Clock;
17
- readonly read?: Omit<CreatePiReadToolOptions, BundleSharedKey>;
18
- readonly edit?: Omit<CreatePiEditToolOptions, BundleSharedKey>;
19
- readonly write?: Omit<CreatePiWriteToolOptions, BundleSharedKey>;
2
+ import type { Clock, Digest, ToolCallContext } from "@better-fs-tools/read";
3
+ import type { ReadSignature } from "@better-fs-tools/read/signature";
4
+ import type { BashSignature } from "@better-fs-tools/shell/signature";
5
+ import type { FsToolsApplyPatchOptions, FsToolsBashOptions, FsToolsEditOptions, FsToolsOptions, FsToolsReadOptions, FsToolsState, FsToolsWriteOptions, InvalidateOutcome, LockManager } from "@better-fs-tools/write";
6
+ import type { EditSignature, PatchSignature, WriteSignature } from "@better-fs-tools/write/signature";
7
+ import type { PiBashTool } from "./bash-tool.ts";
8
+ import type { PiMutationTool } from "./mutation-tools.ts";
9
+ import type { PiPromptOptions } from "./prompt.ts";
10
+ import type { PiReadTool } from "./tool.ts";
11
+ type Host = ExtensionContext;
12
+ /** Read tool options for createPiFsTools. Shared keys are set once at the top level. */
13
+ export type PiFsToolsReadOptions = FsToolsReadOptions<Host> & PiPromptOptions & {
14
+ /** Default defaultReadSignature(). */
15
+ readonly signature?: ReadSignature;
16
+ };
17
+ export type PiFsToolsEditOptions = FsToolsEditOptions<Host> & PiPromptOptions & {
18
+ /** Default defaultEditSignature() for `matchers`. */
19
+ readonly signature?: EditSignature;
20
+ };
21
+ export type PiFsToolsWriteOptions = FsToolsWriteOptions<Host> & PiPromptOptions & {
22
+ /** Default defaultWriteSignature(). */
23
+ readonly signature?: WriteSignature;
24
+ };
25
+ export type PiFsToolsApplyPatchOptions = FsToolsApplyPatchOptions<Host> & PiPromptOptions & {
26
+ /** Default defaultPatchSignature(). */
27
+ readonly signature?: PatchSignature;
28
+ };
29
+ /** Bash tool options for createPiFsTools. `runner` and `env` are required. */
30
+ export type PiFsToolsBashOptions = FsToolsBashOptions<Host> & PiPromptOptions & {
31
+ /** Default defaultBashSignature() for the runner's id and the limits. */
32
+ readonly signature?: BashSignature;
33
+ };
34
+ /**
35
+ * The options of createFsTools(), with the Pi extension context as host, and
36
+ * a `signature` and the Pi prompt options in each tool's options.
37
+ */
38
+ export interface PiFsToolsOptions extends Omit<FsToolsOptions<Host>, "read" | "edit" | "write" | "applyPatch" | "bash"> {
39
+ readonly read?: PiFsToolsReadOptions;
40
+ readonly edit?: PiFsToolsEditOptions;
41
+ readonly write?: PiFsToolsWriteOptions;
20
42
  /** Off by default. `true` or an options object adds an apply_patch tool. */
21
- readonly applyPatch?: boolean | Omit<CreatePiApplyPatchToolOptions, BundleSharedKey>;
43
+ readonly applyPatch?: boolean | PiFsToolsApplyPatchOptions;
44
+ /** Off by default. Give a runner and an env to add a bash tool. */
45
+ readonly bash?: false | PiFsToolsBashOptions;
22
46
  }
23
47
  export interface PiFsTools {
24
48
  readonly read: PiReadTool;
@@ -26,34 +50,44 @@ export interface PiFsTools {
26
50
  readonly write: PiMutationTool;
27
51
  /** null unless options.applyPatch is true or an object. */
28
52
  readonly applyPatch: PiMutationTool | null;
29
- readonly state: ReadStateStore | null;
30
- /** options.digest, or nodeDigest(). */
53
+ /** null unless options.bash is set. */
54
+ readonly bash: PiBashTool | null;
55
+ readonly state: FsToolsState<Host>;
31
56
  readonly digest: Digest;
32
57
  readonly locks: LockManager;
33
58
  readonly clock: Clock;
34
59
  /**
35
- * Forgets the read record for a path under the call's ctx.cwd, so the next
36
- * edit needs a read. Pass the call; in a bash afterRun hook it is ctx.call.
37
- * Without it the outcome is { ok: false } with reason "unsupported". Never
38
- * throws. With state null it reports recorded: false.
60
+ * Forgets the read record for a path, so the next edit needs a read. Pass the
61
+ * call when fs or state is a factory; in a bash afterRun hook it is ctx.call.
62
+ * Never throws.
39
63
  */
40
- invalidate(path: string, call?: ToolCallContext<ExtensionContext>): Promise<InvalidateOutcome>;
64
+ invalidate(path: string, call?: ToolCallContext<Host>): Promise<InvalidateOutcome>;
41
65
  }
42
66
  /** The createPiFsTools result when `applyPatch` is true or an object: it is never null. */
43
67
  export interface PiFsToolsWithApplyPatch extends PiFsTools {
44
68
  readonly applyPatch: PiMutationTool;
45
69
  }
70
+ /** The createPiFsTools result when `bash` is set: it is never null. */
71
+ export interface PiFsToolsWithBash extends PiFsTools {
72
+ readonly bash: PiBashTool;
73
+ }
46
74
  /**
47
- * Creates Pi read, edit, and write tools rooted at each call's ctx.cwd, and
48
- * an apply_patch tool when `applyPatch` is true or an object. They share one
49
- * digest, lock manager, and clock, and the read store when `state` is given.
50
- * Throws TypeError on fs, cwd, or allowedRoots anywhere in the options, on an
51
- * unknown key, and on a shared option (state, digest, locks, clock, or a root
52
- * option) inside one tool's options. There is no bash: register
53
- * createPiBashTool() and call invalidate from its afterRun hook.
75
+ * Creates Pi read, edit, and write tools over one filesystem, sharing one
76
+ * digest, lock manager, and clock, and the read state when `state` is given.
77
+ * With `applyPatch`, also apply_patch. With `bash`, also bash, with the same
78
+ * digest and clock. Every option and default is the one of createFsTools().
79
+ * Throws TypeError where createFsTools() does, with createPiFsTools in the
80
+ * message.
54
81
  */
55
- export declare function createPiFsTools(options: CreatePiFsToolsOptions & {
56
- readonly applyPatch: true | Omit<CreatePiApplyPatchToolOptions, BundleSharedKey>;
82
+ export declare function createPiFsTools(options: PiFsToolsOptions & {
83
+ readonly applyPatch: true | PiFsToolsApplyPatchOptions;
84
+ readonly bash: PiFsToolsBashOptions;
85
+ }): PiFsToolsWithApplyPatch & PiFsToolsWithBash;
86
+ export declare function createPiFsTools(options: PiFsToolsOptions & {
87
+ readonly bash: PiFsToolsBashOptions;
88
+ }): PiFsToolsWithBash;
89
+ export declare function createPiFsTools(options: PiFsToolsOptions & {
90
+ readonly applyPatch: true | PiFsToolsApplyPatchOptions;
57
91
  }): PiFsToolsWithApplyPatch;
58
- export declare function createPiFsTools(options?: CreatePiFsToolsOptions): PiFsTools;
92
+ export declare function createPiFsTools(options: PiFsToolsOptions): PiFsTools;
59
93
  export {};
package/dist/fs-tools.js CHANGED
@@ -1,93 +1,38 @@
1
- import { nodeDigest } from "@better-fs-tools/node";
2
- import { createFsTools } from "@better-fs-tools/write";
3
- import { adaptPiMutationTool, piApplyPatchParts, piEditParts, piWriteParts, } from "./mutation-tools.js";
4
- import { checkPiOptions, PI_ROOT_KEYS, piFileSystems } from "./roots.js";
5
- import { adaptPiReadTool, piReadDeps, piReadParts } from "./tool.js";
6
- const KNOWN_OPTIONS = new Set([
7
- "state",
8
- "digest",
9
- "locks",
10
- "clock",
11
- ...PI_ROOT_KEYS,
12
- "read",
13
- "edit",
14
- "write",
15
- "applyPatch",
16
- ]);
17
- const BUNDLE_SHARED_KEYS = ["state", "digest", "locks", "clock", ...PI_ROOT_KEYS];
18
- export function createPiFsTools(options = {}) {
19
- checkPiOptions(options, "fs");
20
- for (const key of Object.keys(options)) {
21
- if (!KNOWN_OPTIONS.has(key))
22
- throw new TypeError(`Unknown createPiFsTools option: ${key}`);
23
- }
24
- const state = options.state;
25
- if (state !== undefined && state !== null && !isReadStateStore(state)) {
26
- throw new TypeError(`createPiFsTools state must be a read state store or null: a bundle takes one store, not a per-call factory`);
27
- }
28
- const given = {
29
- read: options.read ?? {},
30
- edit: options.edit ?? {},
31
- write: options.write ?? {},
32
- applyPatch: options.applyPatch === true
33
- ? {}
34
- : options.applyPatch === false || options.applyPatch === undefined
35
- ? null
36
- : options.applyPatch,
37
- };
38
- for (const [key, value] of Object.entries(given)) {
39
- if (value === null)
40
- continue;
41
- checkPiOptions(value, key);
42
- for (const shared of BUNDLE_SHARED_KEYS) {
43
- if (Object.hasOwn(value, shared)) {
44
- throw new TypeError(`createPiFsTools ${key} options cannot set ${shared}: set it once at the top level`);
45
- }
46
- }
47
- }
48
- const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode } = options;
49
- const fs = piFileSystems({ denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode });
50
- const { signature: _readSignature, promptSnippet: _readSnippet, promptGuidelines: _readLines, ...read } = given.read;
51
- const { signature: _editSignature, promptSnippet: _editSnippet, promptGuidelines: _editLines, ...edit } = given.edit;
52
- const { signature: _writeSignature, promptSnippet: _writeSnippet, promptGuidelines: _writeLines, ...write } = given.write;
53
- const parts = {
54
- read: piReadParts(given.read),
55
- edit: piEditParts(given.edit),
56
- write: piWriteParts(given.write),
57
- };
58
- let patch = null;
59
- if (given.applyPatch !== null) {
60
- const { signature: _patchSignature, promptSnippet: _patchSnippet, promptGuidelines: _patchLines, ...deps } = given.applyPatch;
61
- patch = { deps, parts: piApplyPatchParts(given.applyPatch) };
62
- }
63
- const core = createFsTools({
64
- fs,
65
- digest: options.digest ?? nodeDigest(),
66
- ...(options.state === undefined ? {} : { state: options.state }),
67
- ...(options.locks === undefined ? {} : { locks: options.locks }),
68
- ...(options.clock === undefined ? {} : { clock: options.clock }),
69
- read: { ...read, ...piReadDeps(parts.read) },
70
- edit: { ...edit, messages: parts.edit.messages },
71
- write: { ...write, messages: parts.write.messages },
72
- ...(patch === null ? {} : { applyPatch: { ...patch.deps, messages: patch.parts.messages } }),
73
- });
1
+ import { fsToolDefinitions } from "@better-fs-tools/write/signature";
2
+ import { adaptPiBashTool } from "./bash-tool.js";
3
+ import { adaptPiMutationTool } from "./mutation-tools.js";
4
+ import { checkPiOptions, splitPrompt } from "./prompt.js";
5
+ import { adaptPiReadTool } from "./tool.js";
6
+ export function createPiFsTools(options) {
7
+ checkPiOptions(options, "createPiFsTools");
8
+ const read = splitPrompt(options.read ?? {});
9
+ const edit = splitPrompt(options.edit ?? {});
10
+ const write = splitPrompt(options.write ?? {});
11
+ const applyPatch = isObject(options.applyPatch) ? splitPrompt(options.applyPatch) : null;
12
+ const bash = isObject(options.bash) ? splitPrompt(options.bash) : null;
13
+ const defs = fsToolDefinitions({
14
+ ...options,
15
+ ...(options.read === undefined ? {} : { read: read.rest }),
16
+ ...(options.edit === undefined ? {} : { edit: edit.rest }),
17
+ ...(options.write === undefined ? {} : { write: write.rest }),
18
+ ...(applyPatch === null ? {} : { applyPatch: applyPatch.rest }),
19
+ ...(bash === null ? {} : { bash: bash.rest }),
20
+ }, "createPiFsTools");
74
21
  return Object.freeze({
75
- read: adaptPiReadTool(parts.read, core.read, core.digest),
76
- edit: adaptPiMutationTool(parts.edit, core.edit),
77
- write: adaptPiMutationTool(parts.write, core.write),
78
- applyPatch: patch === null || core.applyPatch === null
22
+ read: adaptPiReadTool(defs.read, read.prompt),
23
+ edit: adaptPiMutationTool("edit", defs.edit, edit.prompt),
24
+ write: adaptPiMutationTool("write", defs.write, write.prompt),
25
+ applyPatch: defs.applyPatch === null
79
26
  ? null
80
- : adaptPiMutationTool(patch.parts, core.applyPatch),
81
- state: core.state,
82
- digest: core.digest,
83
- locks: core.locks,
84
- clock: core.clock,
85
- invalidate: (path, call) => core.invalidate(path, call),
27
+ : adaptPiMutationTool("apply_patch", defs.applyPatch, applyPatch?.prompt ?? {}),
28
+ bash: defs.bash === null ? null : adaptPiBashTool(defs.bash, bash?.prompt ?? {}),
29
+ state: defs.state,
30
+ digest: defs.digest,
31
+ locks: defs.locks,
32
+ clock: defs.clock,
33
+ invalidate: defs.invalidate,
86
34
  });
87
35
  }
88
- function isReadStateStore(value) {
89
- if (value === null || typeof value !== "object")
90
- return false;
91
- const store = value;
92
- return ["get", "put", "delete"].every((key) => typeof store[key] === "function");
36
+ function isObject(value) {
37
+ return value !== null && typeof value === "object" && !Array.isArray(value);
93
38
  }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  export { createPiBashTool } from "./bash-tool.ts";
2
- export type { CreatePiBashToolOptions, PiBashDetails, PiBashTool, PiBashToolResult, } from "./bash-tool.ts";
2
+ export type { PiBashDetails, PiBashTool, PiBashToolOptions, PiBashToolResult, } from "./bash-tool.ts";
3
3
  export { toPiReadDetails } from "./details.ts";
4
4
  export type { PiReadDetails, PiTruncationResult } from "./details.ts";
5
5
  export { createPiFsTools } from "./fs-tools.ts";
6
- export type { CreatePiFsToolsOptions, PiFsTools, PiFsToolsWithApplyPatch } from "./fs-tools.ts";
6
+ export type { PiFsTools, PiFsToolsApplyPatchOptions, PiFsToolsBashOptions, PiFsToolsEditOptions, PiFsToolsOptions, PiFsToolsReadOptions, PiFsToolsWithApplyPatch, PiFsToolsWithBash, PiFsToolsWriteOptions, } from "./fs-tools.ts";
7
+ export { piMutationQueueLocks } from "./locks.ts";
8
+ export type { PiMutationQueueLocksOptions } from "./locks.ts";
7
9
  export { toPiMutationDetails } from "./mutation-details.ts";
8
10
  export type { PiMutationDetails } from "./mutation-details.ts";
9
11
  export { createPiApplyPatchTool, createPiEditTool, createPiWriteTool } from "./mutation-tools.ts";
10
- export type { CreatePiApplyPatchToolOptions, CreatePiEditToolOptions, CreatePiWriteToolOptions, PiMutationTool, PiMutationToolResult, } from "./mutation-tools.ts";
12
+ export type { PiApplyPatchToolOptions, PiEditToolOptions, PiMutationTool, PiMutationToolResult, PiWriteToolOptions, } from "./mutation-tools.ts";
13
+ export type { PiPromptOptions } from "./prompt.ts";
14
+ export { piToolRenderers } from "./renderers.ts";
15
+ export type { PiToolAnnotations } from "./results.ts";
11
16
  export { createPiReadTool } from "./tool.ts";
12
- export type { CreatePiReadToolOptions, PiContentPart, PiReadTool, PiReadToolResult, } from "./tool.ts";
17
+ export type { PiContentPart, PiReadTool, PiReadToolOptions, PiReadToolResult } from "./tool.ts";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { createPiBashTool } from "./bash-tool.js";
2
2
  export { toPiReadDetails } from "./details.js";
3
3
  export { createPiFsTools } from "./fs-tools.js";
4
+ export { piMutationQueueLocks } from "./locks.js";
4
5
  export { toPiMutationDetails } from "./mutation-details.js";
5
6
  export { createPiApplyPatchTool, createPiEditTool, createPiWriteTool } from "./mutation-tools.js";
7
+ export { piToolRenderers } from "./renderers.js";
6
8
  export { createPiReadTool } from "./tool.js";
@@ -0,0 +1,16 @@
1
+ import type { LockManager } from "@better-fs-tools/write";
2
+ export interface PiMutationQueueLocksOptions {
3
+ /** How long acquire() waits for every key. Default 30 000. */
4
+ readonly timeoutMs?: number;
5
+ }
6
+ /**
7
+ * A lock manager on Pi's withFileMutationQueue(), the per-file queue of Pi's
8
+ * own edit and write. A Better FS Tools edit and a Pi tool that change the same
9
+ * file then wait for each other. The keys are the resolved paths that the
10
+ * write tools pass, which are the real paths that Pi's queue uses.
11
+ *
12
+ * A caller takes the keys one after another in sorted order, inside one queue
13
+ * slot each. On a timeout or an abort the outcome comes at once, and the slots
14
+ * that the caller got later are given back as soon as they arrive.
15
+ */
16
+ export declare function piMutationQueueLocks(options?: PiMutationQueueLocksOptions): LockManager;
package/dist/locks.js ADDED
@@ -0,0 +1,68 @@
1
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
2
+ const DEFAULT_TIMEOUT_MS = 30_000;
3
+ /**
4
+ * A lock manager on Pi's withFileMutationQueue(), the per-file queue of Pi's
5
+ * own edit and write. A Better FS Tools edit and a Pi tool that change the same
6
+ * file then wait for each other. The keys are the resolved paths that the
7
+ * write tools pass, which are the real paths that Pi's queue uses.
8
+ *
9
+ * A caller takes the keys one after another in sorted order, inside one queue
10
+ * slot each. On a timeout or an abort the outcome comes at once, and the slots
11
+ * that the caller got later are given back as soon as they arrive.
12
+ */
13
+ export function piMutationQueueLocks(options = {}) {
14
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
15
+ throw new TypeError("piMutationQueueLocks options must be an object");
16
+ }
17
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
18
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
19
+ throw new TypeError("timeoutMs must be a positive safe integer");
20
+ }
21
+ return Object.freeze({
22
+ id: "pi-file-mutation-queue",
23
+ acquire: (keys, { signal } = {}) => acquire(keys, signal, timeoutMs),
24
+ });
25
+ }
26
+ function acquire(keys, signal, timeoutMs) {
27
+ if (signal?.aborted)
28
+ return Promise.resolve({ ok: false, reason: "aborted" });
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ const settle = (finish) => {
32
+ if (settled)
33
+ return false;
34
+ settled = true;
35
+ clearTimeout(timer);
36
+ signal?.removeEventListener("abort", onAbort);
37
+ finish();
38
+ return true;
39
+ };
40
+ const timer = setTimeout(() => settle(() => resolve({ ok: false, reason: "timeout" })), timeoutMs);
41
+ const onAbort = () => settle(() => resolve({ ok: false, reason: "aborted" }));
42
+ signal?.addEventListener("abort", onAbort, { once: true });
43
+ // Each key's queue slot stays taken until the slot callback returns. The
44
+ // innermost callback holds every slot until release() resolves `held`.
45
+ let release = () => { };
46
+ const held = new Promise((done) => {
47
+ release = done;
48
+ });
49
+ const hold = (index) => {
50
+ if (index < keys.length) {
51
+ return withFileMutationQueue(keys[index], () => hold(index + 1));
52
+ }
53
+ let released = false;
54
+ const granted = settle(() => resolve({
55
+ ok: true,
56
+ release() {
57
+ if (released)
58
+ return;
59
+ released = true;
60
+ release();
61
+ },
62
+ }));
63
+ // After a timeout or an abort, give the slots back at once.
64
+ return granted ? held : Promise.resolve();
65
+ };
66
+ hold(0).catch((error) => settle(() => reject(error)));
67
+ });
68
+ }