@better-fs-tools/pi 0.0.0-tegami-trusted-publish-setup → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +138 -6
- package/dist/bash-tool.d.ts +48 -0
- package/dist/bash-tool.js +76 -0
- package/dist/details.d.ts +26 -0
- package/dist/details.js +30 -0
- package/dist/extension.d.ts +8 -0
- package/dist/extension.js +14 -0
- package/dist/fs-tools.d.ts +49 -0
- package/dist/fs-tools.js +90 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/mutation-details.d.ts +15 -0
- package/dist/mutation-details.js +76 -0
- package/dist/mutation-tools.d.ts +72 -0
- package/dist/mutation-tools.js +117 -0
- package/dist/parts.d.ts +10 -0
- package/dist/parts.js +20 -0
- package/dist/roots.d.ts +20 -0
- package/dist/roots.js +58 -0
- package/dist/tool.d.ts +62 -0
- package/dist/tool.js +81 -0
- package/package.json +64 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gareth Andrew
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,138 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
# @better-fs-tools/pi
|
|
2
|
+
|
|
3
|
+
Better FS Tools for the [Pi coding agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent). It replaces Pi's built-in `read`, `edit`, and `write` tools, and adds `apply_patch`. Every call is confined to the working directory of that call, and `edit` and `write` need a read first.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @better-fs-tools/pi @better-fs-tools/read @better-fs-tools/write @earendil-works/pi-coding-agent typebox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@earendil-works/pi-coding-agent` (`^0.84.2`) and `typebox` (`^1.3.16`) are required peers. The package needs Node 24 or later.
|
|
12
|
+
|
|
13
|
+
The package has a `pi.extensions` entry. When Pi loads the package, the entry registers `read`, `edit`, `write`, and `apply_patch` from `createPiFsTools()`. The four tools share one in-memory read store, so `edit` and `write` need a read first. Writes stay inside `ctx.cwd`. The entry writes no settings or session file.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
To choose your own options, register the tools from your own extension:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { createPiFsTools } from "@better-fs-tools/pi";
|
|
22
|
+
import { denyPaths, unicodeRepair } from "@better-fs-tools/read";
|
|
23
|
+
import { hashlineFormat } from "@better-fs-tools/read/formats";
|
|
24
|
+
import { protectPaths } from "@better-fs-tools/write";
|
|
25
|
+
|
|
26
|
+
export default function fsToolsExtension(pi: ExtensionAPI): void {
|
|
27
|
+
// Four tools with one read store, so edit and write need a read first.
|
|
28
|
+
const tools = createPiFsTools({
|
|
29
|
+
read: {
|
|
30
|
+
resolve: unicodeRepair({ note: false }),
|
|
31
|
+
authorize: denyPaths(["**/.env", "**/.env.*"]),
|
|
32
|
+
formatter: hashlineFormat(),
|
|
33
|
+
},
|
|
34
|
+
edit: { authorize: protectPaths() },
|
|
35
|
+
write: { authorize: protectPaths() },
|
|
36
|
+
applyPatch: { authorize: protectPaths() },
|
|
37
|
+
});
|
|
38
|
+
pi.registerTool(tools.read);
|
|
39
|
+
pi.registerTool(tools.edit);
|
|
40
|
+
pi.registerTool(tools.write);
|
|
41
|
+
pi.registerTool(tools.applyPatch);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To register only the read tool:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
49
|
+
import { createPiReadTool } from "@better-fs-tools/pi";
|
|
50
|
+
import { denyPaths, unicodeRepair } from "@better-fs-tools/read";
|
|
51
|
+
import { hashlineFormat } from "@better-fs-tools/read/formats";
|
|
52
|
+
import { defaultReadSignature } from "@better-fs-tools/read/signature";
|
|
53
|
+
|
|
54
|
+
export default function readExtension(pi: ExtensionAPI): void {
|
|
55
|
+
pi.registerTool(
|
|
56
|
+
createPiReadTool({
|
|
57
|
+
signature: defaultReadSignature({ name: "read", names: { path: "file_path" } }),
|
|
58
|
+
resolve: unicodeRepair({ note: false }),
|
|
59
|
+
authorize: denyPaths(["**/.env", "**/.env.*"]),
|
|
60
|
+
formatter: hashlineFormat(),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What the read tool does
|
|
67
|
+
|
|
68
|
+
- `createPiReadTool(options?)` takes every `createReadTool()` option except `fs`. It also takes `signature`, `promptSnippet`, `promptGuidelines`, `denyRoots`, and `symlinks`.
|
|
69
|
+
- `fs`, `cwd`, and `allowedRoots` throw `TypeError`. Each call reads through `nodeFileSystem` with Pi's `ctx.cwd` as the only allowed root. When the working directory changes between calls, the root changes too. The tool keeps the filesystems for the last 8 working directories.
|
|
70
|
+
- Pi's `ExtensionContext` is the host type. Host functions get it as `ctx.call.host`, for example `ctx.call.host.ui.confirm(...)` in `askUser`, or `ctx.call.host.sessionManager.getSessionId()` in a `state` factory.
|
|
71
|
+
- The default signature is `defaultReadSignature({ name: "read" })`. Its schema has integer `offset` and `limit`, so a model that sends `2.5` gets Pi's validation error. `digest` defaults to `nodeDigest()`.
|
|
72
|
+
- Image parts become Pi `image` parts. Other media becomes a text part that says what was left out.
|
|
73
|
+
- `details.truncation` follows Pi's own read tool for line and byte stops. Its `content` comes from the formatter in `"view"` mode. For other results, or a formatter that returns parts, `details` is `{}`. `toPiReadDetails()` builds the details for your own tool.
|
|
74
|
+
|
|
75
|
+
## What the write tools do
|
|
76
|
+
|
|
77
|
+
- `createPiFsTools(options?)` wraps `createFsTools()` from `@better-fs-tools/write`. It builds `read`, `edit`, `write`, and `apply_patch` with one `memoryStore({ clock })`, one `nodeDigest()`, one lock manager, one clock, and one cache of filesystems over `ctx.cwd`. `state: null` turns read-before-write off. `digest` (default `nodeDigest()`), `locks`, `clock`, and the root options (`denyRoots`, `symlinks`, `hardLinks`, `newFileMode`, `newDirectoryMode`) are set once for all four. Each tool's other options go under `read`, `edit`, `write`, and `applyPatch`. The result also has `state`, `digest`, `locks`, `clock`, and `invalidate(path, call?)`, which returns an `InvalidateOutcome`. Pass the call, so the path resolves under its `ctx.cwd`: in a bash `afterRun` hook it is `ctx.call`. Without it the outcome is `{ ok: false }` with reason `unsupported`. An unknown top-level key, or `state`, `digest`, `locks`, `clock`, or a root option inside a tool's options, throws `TypeError`.
|
|
78
|
+
- `createPiEditTool()`, `createPiWriteTool()`, and `createPiApplyPatchTool()` build one tool each. They default to `state: null`, so every update carries a `read-before-write-off` note. Pass the same `state` as your read tool, or use `createPiFsTools()`.
|
|
79
|
+
- `fs`, `cwd`, and `allowedRoots` throw `TypeError`, as for the read tool. Writes go through `nodeFileSystem` with `ctx.cwd` as the only allowed root, and `hardLinks: "refuse"` by default. `newFileMode` and `newDirectoryMode` set the modes of new files and directories exactly. Without them, a new file is `0o666` and a new directory `0o777`, less the process umask.
|
|
80
|
+
- The default signatures follow Pi's own tools: `edit` takes `path` and `edits: [{ oldText, newText }]` (`multiEditSignature({ name: "edit" })`), and `write` takes `path` and `content`. Their prompt snippets and guidelines are Pi's own, so the system prompt stays the same. `apply_patch` uses `freeformPatchSignature()`: models with grammar-tool support get the Codex patch grammar, and other models get the JSON schema.
|
|
81
|
+
- `edit` and `apply_patch` return `details` in the shape of Pi's `EditToolDetails` (`diff`, `patch`, `firstChangedLine`), so Pi's edit renderer draws the diff. `toPiMutationDetails()` builds them for your own tool. `write` returns no details, as Pi's own `write` does.
|
|
82
|
+
- A tool error becomes Pi content text. The tool does not throw for a tool error.
|
|
83
|
+
- Pi's `edit` guideline says the old text must match exactly. The tool also accepts close matches, and the result says when it used one.
|
|
84
|
+
|
|
85
|
+
## Bash tool
|
|
86
|
+
|
|
87
|
+
`createPiBashTool()` is a `bash` tool from [`@better-fs-tools/shell`](https://www.npmjs.com/package/@better-fs-tools/shell) with Pi's own shape: `{ command, timeout }` with the timeout in seconds, and Pi's prompt snippet. It runs in `ctx.cwd` on every call, also with a `runner` you pass: the runner gets `ctx.cwd` as the request's `cwd`, not its own. A relative `ctx.cwd` throws `TypeError`. The extension entry does not register it, so Pi's own `bash` stays unless you register this one:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
91
|
+
import { createPiBashTool } from "@better-fs-tools/pi";
|
|
92
|
+
|
|
93
|
+
// Replaces Pi's own bash: same name, { command, timeout } in seconds, run in ctx.cwd.
|
|
94
|
+
export default function bashExtension(pi: ExtensionAPI): void {
|
|
95
|
+
pi.registerTool(createPiBashTool());
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The options take every `createBashTool()` dependency except `cwd`, plus `runner`, `signature`, `promptSnippet`, and `promptGuidelines`. `runner` and `env` are optional here. The default description names the timeouts and output limits from `limits`, and the id of a `runner` object when you pass one.
|
|
100
|
+
|
|
101
|
+
Pi's own `bash` also sets `PI_*` session variables. This tool does not. Add them with the `env` option when you need them. `env` defaults to `shellEnv(() => process.env)`.
|
|
102
|
+
|
|
103
|
+
With the file tools, a bash `afterRun` hook can make the next edit of a file need a read:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
107
|
+
import { createPiBashTool, createPiFsTools } from "@better-fs-tools/pi";
|
|
108
|
+
|
|
109
|
+
// The four file tools and Pi-shaped bash. After each command, the next edit of
|
|
110
|
+
// package.json needs a read, since the command may have changed it.
|
|
111
|
+
export default function fsAndBashExtension(pi: ExtensionAPI): void {
|
|
112
|
+
const tools = createPiFsTools();
|
|
113
|
+
const bash = createPiBashTool({
|
|
114
|
+
afterRun: [
|
|
115
|
+
{
|
|
116
|
+
id: "invalidate-package-json",
|
|
117
|
+
afterRun: async (_outcome, ctx) => {
|
|
118
|
+
// ctx.call carries Pi's ctx, so the path resolves under its cwd.
|
|
119
|
+
await tools.invalidate("package.json", ctx.call);
|
|
120
|
+
return {};
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
pi.registerTool(tools.read);
|
|
126
|
+
pi.registerTool(tools.edit);
|
|
127
|
+
pi.registerTool(tools.write);
|
|
128
|
+
pi.registerTool(tools.applyPatch);
|
|
129
|
+
pi.registerTool(bash);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Links
|
|
134
|
+
|
|
135
|
+
- `docs/hosts.md` in [`@better-fs-tools/write`](https://www.npmjs.com/package/@better-fs-tools/write): the defaults of every host and bundle, and what each backend can do
|
|
136
|
+
- [`@better-fs-tools/read`](https://www.npmjs.com/package/@better-fs-tools/read): every read option, the signature builders, and the format presets. Its README has a full Pi host example.
|
|
137
|
+
- [`@better-fs-tools/write`](https://www.npmjs.com/package/@better-fs-tools/write): every write option, the guards, and the authorizers
|
|
138
|
+
- [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node): the filesystem that each call uses
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
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";
|
|
6
|
+
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
|
+
}
|
|
22
|
+
/** Pi's BashToolDetails shape, so Pi's bash renderer can show the saved output. */
|
|
23
|
+
export interface PiBashDetails {
|
|
24
|
+
fullOutputPath?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PiBashToolResult {
|
|
27
|
+
content: PiContentPart[];
|
|
28
|
+
details: PiBashDetails | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Assignable to ToolDefinition<TSchema, PiBashDetails | undefined> from Pi 0.84.2. */
|
|
31
|
+
export interface PiBashTool {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly description: string;
|
|
35
|
+
readonly promptSnippet: string;
|
|
36
|
+
readonly promptGuidelines: string[];
|
|
37
|
+
/** The JSON Schema of the tool input. */
|
|
38
|
+
readonly parameters: TSchema;
|
|
39
|
+
execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiBashToolResult>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
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.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createPiBashTool(options?: CreatePiBashToolOptions): PiBashTool;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
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";
|
|
6
|
+
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;
|
|
10
|
+
/**
|
|
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.
|
|
16
|
+
*/
|
|
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),
|
|
42
|
+
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
|
+
const call = {
|
|
47
|
+
...(signal === undefined ? {} : { signal }),
|
|
48
|
+
callId: toolCallId,
|
|
49
|
+
host: ctx,
|
|
50
|
+
};
|
|
51
|
+
const result = await bash(signature.toInput(input), call);
|
|
52
|
+
const spill = result.output?.spill ?? null;
|
|
53
|
+
return {
|
|
54
|
+
content: result.content.map((part) => toPiPart(part, "bash")),
|
|
55
|
+
details: spill === null ? undefined : { fullOutputPath: spill },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
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
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReadResult } from "@better-fs-tools/read";
|
|
2
|
+
/** Pi's published TruncationResult. Fields and meanings are Pi's. */
|
|
3
|
+
export interface PiTruncationResult {
|
|
4
|
+
content: string;
|
|
5
|
+
truncated: boolean;
|
|
6
|
+
truncatedBy: "lines" | "bytes" | null;
|
|
7
|
+
totalLines: number;
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
outputLines: number;
|
|
10
|
+
outputBytes: number;
|
|
11
|
+
lastLinePartial: boolean;
|
|
12
|
+
firstLineExceedsLimit: boolean;
|
|
13
|
+
maxLines: number;
|
|
14
|
+
maxBytes: number;
|
|
15
|
+
}
|
|
16
|
+
/** Empty unless the read stopped at exactly a line or byte limit. */
|
|
17
|
+
export interface PiReadDetails {
|
|
18
|
+
truncation?: PiTruncationResult;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Converts a read result to Pi's read details, from which Pi prints "showing
|
|
22
|
+
* N of M lines". `view` is the formatted text in "view" mode, or null. Returns
|
|
23
|
+
* {} unless the read stopped exactly at a line or byte limit with an exact
|
|
24
|
+
* total, because an approximate count is worse than none.
|
|
25
|
+
*/
|
|
26
|
+
export declare function toPiReadDetails(result: ReadResult, view: string | null, maxViewBytes: number): PiReadDetails;
|
package/dist/details.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a read result to Pi's read details, from which Pi prints "showing
|
|
3
|
+
* N of M lines". `view` is the formatted text in "view" mode, or null. Returns
|
|
4
|
+
* {} unless the read stopped exactly at a line or byte limit with an exact
|
|
5
|
+
* total, because an approximate count is worse than none.
|
|
6
|
+
*/
|
|
7
|
+
export function toPiReadDetails(result, view, maxViewBytes) {
|
|
8
|
+
if (view === null)
|
|
9
|
+
return {};
|
|
10
|
+
if (result.status !== "ok" || !result.totals.exact || result.totals.lines === null)
|
|
11
|
+
return {};
|
|
12
|
+
const primary = result.truncation.primary;
|
|
13
|
+
if (!result.truncation.truncated || (primary !== "lines" && primary !== "bytes"))
|
|
14
|
+
return {};
|
|
15
|
+
return {
|
|
16
|
+
truncation: {
|
|
17
|
+
content: view,
|
|
18
|
+
truncated: true,
|
|
19
|
+
truncatedBy: primary,
|
|
20
|
+
totalLines: result.totals.lines,
|
|
21
|
+
totalBytes: result.totals.bytes,
|
|
22
|
+
outputLines: result.view.lines.length,
|
|
23
|
+
outputBytes: result.view.bytes,
|
|
24
|
+
lastLinePartial: false,
|
|
25
|
+
firstLineExceedsLimit: primary === "bytes" && result.view.lines.length === 0,
|
|
26
|
+
maxLines: result.request.limit,
|
|
27
|
+
maxBytes: maxViewBytes,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PiMutationTool } from "./mutation-tools.ts";
|
|
2
|
+
import type { PiReadTool } from "./tool.ts";
|
|
3
|
+
/** The part of Pi's ExtensionAPI this extension uses. Pi's full API satisfies it. */
|
|
4
|
+
export interface PiExtensionApi {
|
|
5
|
+
registerTool(tool: PiReadTool): void;
|
|
6
|
+
registerTool(tool: PiMutationTool): void;
|
|
7
|
+
}
|
|
8
|
+
export default function fsToolsExtension(pi: PiExtensionApi): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi package extension entry. Installing the package into a Pi project
|
|
3
|
+
* registers read, edit, write, and apply_patch, and nothing else. The four
|
|
4
|
+
* tools share one in-memory read store, so edit and write need a read first.
|
|
5
|
+
* Nothing is written outside the process: no settings file, no session file.
|
|
6
|
+
*/
|
|
7
|
+
import { createPiFsTools } from "./fs-tools.js";
|
|
8
|
+
export default function fsToolsExtension(pi) {
|
|
9
|
+
const tools = createPiFsTools();
|
|
10
|
+
pi.registerTool(tools.read);
|
|
11
|
+
pi.registerTool(tools.edit);
|
|
12
|
+
pi.registerTool(tools.write);
|
|
13
|
+
pi.registerTool(tools.applyPatch);
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 a memory store on the bundle clock. null turns read-before-write off. */
|
|
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>;
|
|
20
|
+
readonly applyPatch?: Omit<CreatePiApplyPatchToolOptions, BundleSharedKey>;
|
|
21
|
+
}
|
|
22
|
+
export interface PiFsTools {
|
|
23
|
+
readonly read: PiReadTool;
|
|
24
|
+
readonly edit: PiMutationTool;
|
|
25
|
+
readonly write: PiMutationTool;
|
|
26
|
+
readonly applyPatch: PiMutationTool;
|
|
27
|
+
readonly state: ReadStateStore | null;
|
|
28
|
+
/** options.digest, or nodeDigest(). */
|
|
29
|
+
readonly digest: Digest;
|
|
30
|
+
readonly locks: LockManager;
|
|
31
|
+
readonly clock: Clock;
|
|
32
|
+
/**
|
|
33
|
+
* Forgets the read record for a path under the call's ctx.cwd, so the next
|
|
34
|
+
* edit needs a read. Pass the call; in a bash afterRun hook it is ctx.call.
|
|
35
|
+
* Without it the outcome is { ok: false } with reason "unsupported". Never
|
|
36
|
+
* throws. With state null it reports recorded: false.
|
|
37
|
+
*/
|
|
38
|
+
invalidate(path: string, call?: ToolCallContext<ExtensionContext>): Promise<InvalidateOutcome>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates Pi read, edit, write, and apply_patch tools rooted at each call's
|
|
42
|
+
* ctx.cwd. They share one read state store, digest, lock manager, and clock.
|
|
43
|
+
* Throws TypeError on fs, cwd, or allowedRoots anywhere in the options, on an
|
|
44
|
+
* unknown key, and on a shared option (state, digest, locks, clock, or a root
|
|
45
|
+
* option) inside one tool's options. There is no bash: register
|
|
46
|
+
* createPiBashTool() and call invalidate from its afterRun hook.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createPiFsTools(options?: CreatePiFsToolsOptions): PiFsTools;
|
|
49
|
+
export {};
|
package/dist/fs-tools.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* Creates Pi read, edit, write, and apply_patch tools rooted at each call's
|
|
20
|
+
* ctx.cwd. They share one read state store, digest, lock manager, and clock.
|
|
21
|
+
* Throws TypeError on fs, cwd, or allowedRoots anywhere in the options, on an
|
|
22
|
+
* unknown key, and on a shared option (state, digest, locks, clock, or a root
|
|
23
|
+
* option) inside one tool's options. There is no bash: register
|
|
24
|
+
* createPiBashTool() and call invalidate from its afterRun hook.
|
|
25
|
+
*/
|
|
26
|
+
export function createPiFsTools(options = {}) {
|
|
27
|
+
checkPiOptions(options, "fs");
|
|
28
|
+
for (const key of Object.keys(options)) {
|
|
29
|
+
if (!KNOWN_OPTIONS.has(key))
|
|
30
|
+
throw new TypeError(`Unknown createPiFsTools option: ${key}`);
|
|
31
|
+
}
|
|
32
|
+
const state = options.state;
|
|
33
|
+
if (state !== undefined && state !== null && !isReadStateStore(state)) {
|
|
34
|
+
throw new TypeError(`createPiFsTools state must be a read state store or null: a bundle takes one store, not a per-call factory`);
|
|
35
|
+
}
|
|
36
|
+
const given = {
|
|
37
|
+
read: options.read ?? {},
|
|
38
|
+
edit: options.edit ?? {},
|
|
39
|
+
write: options.write ?? {},
|
|
40
|
+
applyPatch: options.applyPatch ?? {},
|
|
41
|
+
};
|
|
42
|
+
for (const [key, value] of Object.entries(given)) {
|
|
43
|
+
checkPiOptions(value, key);
|
|
44
|
+
for (const shared of BUNDLE_SHARED_KEYS) {
|
|
45
|
+
if (Object.hasOwn(value, shared)) {
|
|
46
|
+
throw new TypeError(`createPiFsTools ${key} options cannot set ${shared}: set it once at the top level`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode } = options;
|
|
51
|
+
const fs = piFileSystems({ denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode });
|
|
52
|
+
const { signature: _readSignature, promptSnippet: _readSnippet, promptGuidelines: _readLines, ...read } = given.read;
|
|
53
|
+
const { signature: _editSignature, promptSnippet: _editSnippet, promptGuidelines: _editLines, ...edit } = given.edit;
|
|
54
|
+
const { signature: _writeSignature, promptSnippet: _writeSnippet, promptGuidelines: _writeLines, ...write } = given.write;
|
|
55
|
+
const { signature: _patchSignature, promptSnippet: _patchSnippet, promptGuidelines: _patchLines, ...applyPatch } = given.applyPatch;
|
|
56
|
+
const parts = {
|
|
57
|
+
read: piReadParts(given.read),
|
|
58
|
+
edit: piEditParts(given.edit),
|
|
59
|
+
write: piWriteParts(given.write),
|
|
60
|
+
applyPatch: piApplyPatchParts(given.applyPatch),
|
|
61
|
+
};
|
|
62
|
+
const core = createFsTools({
|
|
63
|
+
fs,
|
|
64
|
+
digest: options.digest ?? nodeDigest(),
|
|
65
|
+
...(options.state === undefined ? {} : { state: options.state }),
|
|
66
|
+
...(options.locks === undefined ? {} : { locks: options.locks }),
|
|
67
|
+
...(options.clock === undefined ? {} : { clock: options.clock }),
|
|
68
|
+
read: { ...read, ...piReadDeps(parts.read) },
|
|
69
|
+
edit: { ...edit, messages: parts.edit.messages },
|
|
70
|
+
write: { ...write, messages: parts.write.messages },
|
|
71
|
+
applyPatch: { ...applyPatch, messages: parts.applyPatch.messages },
|
|
72
|
+
});
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
read: adaptPiReadTool(parts.read, core.read, core.digest),
|
|
75
|
+
edit: adaptPiMutationTool(parts.edit, core.edit),
|
|
76
|
+
write: adaptPiMutationTool(parts.write, core.write),
|
|
77
|
+
applyPatch: adaptPiMutationTool(parts.applyPatch, core.applyPatch),
|
|
78
|
+
state: core.state,
|
|
79
|
+
digest: core.digest,
|
|
80
|
+
locks: core.locks,
|
|
81
|
+
clock: core.clock,
|
|
82
|
+
invalidate: (path, call) => core.invalidate(path, call),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function isReadStateStore(value) {
|
|
86
|
+
if (value === null || typeof value !== "object")
|
|
87
|
+
return false;
|
|
88
|
+
const store = value;
|
|
89
|
+
return ["get", "put", "delete"].every((key) => typeof store[key] === "function");
|
|
90
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createPiBashTool } from "./bash-tool.ts";
|
|
2
|
+
export type { CreatePiBashToolOptions, PiBashDetails, PiBashTool, PiBashToolResult, } from "./bash-tool.ts";
|
|
3
|
+
export { toPiReadDetails } from "./details.ts";
|
|
4
|
+
export type { PiReadDetails, PiTruncationResult } from "./details.ts";
|
|
5
|
+
export { createPiFsTools } from "./fs-tools.ts";
|
|
6
|
+
export type { CreatePiFsToolsOptions, PiFsTools } from "./fs-tools.ts";
|
|
7
|
+
export { toPiMutationDetails } from "./mutation-details.ts";
|
|
8
|
+
export type { PiMutationDetails } from "./mutation-details.ts";
|
|
9
|
+
export { createPiApplyPatchTool, createPiEditTool, createPiWriteTool } from "./mutation-tools.ts";
|
|
10
|
+
export type { CreatePiApplyPatchToolOptions, CreatePiEditToolOptions, CreatePiWriteToolOptions, PiMutationTool, PiMutationToolResult, } from "./mutation-tools.ts";
|
|
11
|
+
export { createPiReadTool } from "./tool.ts";
|
|
12
|
+
export type { CreatePiReadToolOptions, PiContentPart, PiReadTool, PiReadToolResult, } from "./tool.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createPiBashTool } from "./bash-tool.js";
|
|
2
|
+
export { toPiReadDetails } from "./details.js";
|
|
3
|
+
export { createPiFsTools } from "./fs-tools.js";
|
|
4
|
+
export { toPiMutationDetails } from "./mutation-details.js";
|
|
5
|
+
export { createPiApplyPatchTool, createPiEditTool, createPiWriteTool } from "./mutation-tools.js";
|
|
6
|
+
export { createPiReadTool } from "./tool.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MutationResult } from "@better-fs-tools/write";
|
|
2
|
+
/** Pi's EditToolDetails shape, so Pi's built-in edit renderer draws the diff. */
|
|
3
|
+
export interface PiMutationDetails {
|
|
4
|
+
/** Pi's display diff, with lines like "+NN text", "-NN text", " NN text", and "...". */
|
|
5
|
+
diff: string;
|
|
6
|
+
/** Unified diff of every change. */
|
|
7
|
+
patch: string;
|
|
8
|
+
firstChangedLine?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Converts an edit or apply_patch result to Pi's edit details, so Pi draws the
|
|
12
|
+
* diff. Returns undefined for an error or a result with no changes. With more
|
|
13
|
+
* than one file, each file's lines follow a line with its path.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toPiMutationDetails(result: MutationResult): PiMutationDetails | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/u;
|
|
2
|
+
/**
|
|
3
|
+
* Converts an edit or apply_patch result to Pi's edit details, so Pi draws the
|
|
4
|
+
* diff. Returns undefined for an error or a result with no changes. With more
|
|
5
|
+
* than one file, each file's lines follow a line with its path.
|
|
6
|
+
*/
|
|
7
|
+
export function toPiMutationDetails(result) {
|
|
8
|
+
if (result.status !== "ok" || result.changes.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const blocks = [];
|
|
11
|
+
let firstChangedLine;
|
|
12
|
+
for (const change of result.changes) {
|
|
13
|
+
const display = toPiDisplayDiff(change.diff);
|
|
14
|
+
firstChangedLine ??= display.firstChangedLine;
|
|
15
|
+
const header = result.changes.length > 1 ? `${label(change)}\n` : "";
|
|
16
|
+
blocks.push(`${header}${display.text}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
diff: blocks.join("\n"),
|
|
20
|
+
patch: result.changes.map((change) => change.diff).join(""),
|
|
21
|
+
...(firstChangedLine === undefined ? {} : { firstChangedLine }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function label(change) {
|
|
25
|
+
return change.movedFrom === null ? change.path : `${change.movedFrom} → ${change.path}`;
|
|
26
|
+
}
|
|
27
|
+
// Context and removed lines carry the old line number, added lines the new one, as Pi's generateDiffString does.
|
|
28
|
+
function toPiDisplayDiff(unified) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
let oldLine = 0;
|
|
31
|
+
let newLine = 0;
|
|
32
|
+
let inHunk = false;
|
|
33
|
+
let firstChangedLine;
|
|
34
|
+
for (const raw of unified.split("\n")) {
|
|
35
|
+
const hunk = HUNK_HEADER.exec(raw);
|
|
36
|
+
if (hunk !== null) {
|
|
37
|
+
if (inHunk)
|
|
38
|
+
rows.push({ kind: "...", line: 0, text: "" });
|
|
39
|
+
inHunk = true;
|
|
40
|
+
oldLine = firstLineOfRange(hunk[1], hunk[2]);
|
|
41
|
+
newLine = firstLineOfRange(hunk[3], hunk[4]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!inHunk || raw.startsWith("\\"))
|
|
45
|
+
continue;
|
|
46
|
+
const kind = raw[0];
|
|
47
|
+
const text = raw.slice(1);
|
|
48
|
+
if (kind === "+") {
|
|
49
|
+
firstChangedLine ??= newLine;
|
|
50
|
+
rows.push({ kind, line: newLine, text });
|
|
51
|
+
newLine += 1;
|
|
52
|
+
}
|
|
53
|
+
else if (kind === "-") {
|
|
54
|
+
firstChangedLine ??= newLine;
|
|
55
|
+
rows.push({ kind, line: oldLine, text });
|
|
56
|
+
oldLine += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (kind === " ") {
|
|
59
|
+
rows.push({ kind, line: oldLine, text });
|
|
60
|
+
oldLine += 1;
|
|
61
|
+
newLine += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const width = String(Math.max(1, ...rows.map((row) => row.line))).length;
|
|
65
|
+
const text = rows
|
|
66
|
+
.map((row) => row.kind === "..."
|
|
67
|
+
? ` ${"".padStart(width, " ")} ...`
|
|
68
|
+
: `${row.kind}${String(row.line).padStart(width, " ")} ${row.text}`)
|
|
69
|
+
.join("\n");
|
|
70
|
+
return { text, firstChangedLine };
|
|
71
|
+
}
|
|
72
|
+
// In a unified diff, a range with a zero count names the line before it.
|
|
73
|
+
function firstLineOfRange(line, count) {
|
|
74
|
+
const value = Number(line);
|
|
75
|
+
return count === "0" ? value + 1 : value;
|
|
76
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import type { JsonObject, StateNeedsDigestOrDefault, ToolCallContext } from "@better-fs-tools/read";
|
|
4
|
+
import type { ApplyPatchToolDeps, EditToolDeps, MutationResult, WriteMessageCatalog, WriteToolDeps } from "@better-fs-tools/write";
|
|
5
|
+
import type { EditSignature, MutationSignature, PatchSignature, WriteSignature } from "@better-fs-tools/write/signature";
|
|
6
|
+
import type { PiMutationDetails } from "./mutation-details.ts";
|
|
7
|
+
import type { PiContentPart } from "./parts.ts";
|
|
8
|
+
import type { PiRootOptions } from "./roots.ts";
|
|
9
|
+
/** Pi prompt options of the edit, write, and apply_patch tools. */
|
|
10
|
+
export interface PiMutationOptions<TSignature> {
|
|
11
|
+
readonly signature?: TSignature;
|
|
12
|
+
readonly promptSnippet?: string;
|
|
13
|
+
readonly promptGuidelines?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Options for createPiEditTool. The default signature is Pi's own edit input,
|
|
17
|
+
* multiEditSignature({ matchers }), so Pi's edit renderer and prompt fit.
|
|
18
|
+
* digest defaults to nodeDigest(); a state needs a digest that is not null.
|
|
19
|
+
*/
|
|
20
|
+
export type CreatePiEditToolOptions = Omit<EditToolDeps<ExtensionContext>, "fs"> & StateNeedsDigestOrDefault & PiRootOptions & PiMutationOptions<EditSignature>;
|
|
21
|
+
/** As CreatePiEditToolOptions, for write. The default signature is defaultWriteSignature(). */
|
|
22
|
+
export type CreatePiWriteToolOptions = Omit<WriteToolDeps<ExtensionContext>, "fs"> & StateNeedsDigestOrDefault & PiRootOptions & PiMutationOptions<WriteSignature>;
|
|
23
|
+
/** As CreatePiEditToolOptions, for apply_patch. The default signature is freeformPatchSignature(). */
|
|
24
|
+
export type CreatePiApplyPatchToolOptions = Omit<ApplyPatchToolDeps<ExtensionContext>, "fs"> & StateNeedsDigestOrDefault & PiRootOptions & PiMutationOptions<PatchSignature>;
|
|
25
|
+
export interface PiMutationToolResult {
|
|
26
|
+
content: PiContentPart[];
|
|
27
|
+
details: PiMutationDetails | undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Assignable to ToolDefinition<TSchema, PiMutationDetails | undefined> from Pi 0.84.2. */
|
|
30
|
+
export interface PiMutationTool {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly description: string;
|
|
34
|
+
readonly promptSnippet: string;
|
|
35
|
+
readonly promptGuidelines: string[];
|
|
36
|
+
/** The JSON Schema of the tool input. */
|
|
37
|
+
readonly parameters: TSchema;
|
|
38
|
+
/** Set when the signature has a grammar. */
|
|
39
|
+
readonly constrainedSampling?: {
|
|
40
|
+
type: "grammar";
|
|
41
|
+
variants: {
|
|
42
|
+
openai_lark: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiMutationToolResult>;
|
|
46
|
+
}
|
|
47
|
+
type Core = (input: never, ctx: ToolCallContext<ExtensionContext>) => Promise<MutationResult>;
|
|
48
|
+
export interface PiMutationParts {
|
|
49
|
+
readonly tool: "edit" | "write" | "apply_patch";
|
|
50
|
+
readonly signature: MutationSignature<unknown>;
|
|
51
|
+
readonly promptSnippet: string;
|
|
52
|
+
readonly promptGuidelines: readonly string[];
|
|
53
|
+
readonly messages: Partial<WriteMessageCatalog>;
|
|
54
|
+
}
|
|
55
|
+
type PartsInput<TSignature> = PiMutationOptions<TSignature> & {
|
|
56
|
+
readonly messages?: Partial<WriteMessageCatalog>;
|
|
57
|
+
};
|
|
58
|
+
export declare function piEditParts(options: PartsInput<EditSignature> & Pick<Partial<EditToolDeps>, "matchers">): PiMutationParts;
|
|
59
|
+
export declare function piWriteParts(options: PartsInput<WriteSignature>): PiMutationParts;
|
|
60
|
+
export declare function piApplyPatchParts(options: PartsInput<PatchSignature>): PiMutationParts;
|
|
61
|
+
/**
|
|
62
|
+
* Creates a Pi edit tool rooted at each call's ctx.cwd. Throws TypeError when
|
|
63
|
+
* options set fs, cwd, or allowedRoots. Read-before-write is off unless you
|
|
64
|
+
* pass a `state`; use createPiFsTools() for a store shared with read.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createPiEditTool(options?: CreatePiEditToolOptions): PiMutationTool;
|
|
67
|
+
/** Creates a Pi write tool, as createPiEditTool. `details` is always undefined, as for Pi's own write. */
|
|
68
|
+
export declare function createPiWriteTool(options?: CreatePiWriteToolOptions): PiMutationTool;
|
|
69
|
+
/** Creates a Pi apply_patch tool, as createPiEditTool. A grammar signature sets constrainedSampling. */
|
|
70
|
+
export declare function createPiApplyPatchTool(options?: CreatePiApplyPatchToolOptions): PiMutationTool;
|
|
71
|
+
export declare function adaptPiMutationTool(made: PiMutationParts, tool: Core): PiMutationTool;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { nodeDigest } from "@better-fs-tools/node";
|
|
3
|
+
import { createApplyPatchTool, createEditTool, createWriteTool } from "@better-fs-tools/write";
|
|
4
|
+
import { defaultWriteSignature, freeformPatchSignature, multiEditSignature, writeSignatureMessages, } from "@better-fs-tools/write/signature";
|
|
5
|
+
import { toPiMutationDetails } from "./mutation-details.js";
|
|
6
|
+
import { toPiPart } from "./parts.js";
|
|
7
|
+
import { checkPiContext, checkPiOptions, piFileSystems } from "./roots.js";
|
|
8
|
+
const PI_BUILTIN_EDIT_SNIPPET = "Make precise file edits with exact text replacement, including multiple disjoint edits in one call";
|
|
9
|
+
const EDIT_GUIDELINES = [
|
|
10
|
+
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
11
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
12
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
|
13
|
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
14
|
+
];
|
|
15
|
+
const PI_BUILTIN_WRITE_SNIPPET = "Create or overwrite files";
|
|
16
|
+
const WRITE_GUIDELINES = ["Use write only for new files or complete rewrites."];
|
|
17
|
+
const PATCH_SNIPPET = "Add, update, move, or delete files with one patch";
|
|
18
|
+
const PATCH_GUIDELINES = [
|
|
19
|
+
"Use apply_patch for related changes across several files, or to add, move, or delete files.",
|
|
20
|
+
];
|
|
21
|
+
export function piEditParts(options) {
|
|
22
|
+
const signature = options.signature ??
|
|
23
|
+
multiEditSignature(options.matchers === undefined ? {} : { matchers: options.matchers });
|
|
24
|
+
return parts("edit", signature, options, PI_BUILTIN_EDIT_SNIPPET, EDIT_GUIDELINES);
|
|
25
|
+
}
|
|
26
|
+
export function piWriteParts(options) {
|
|
27
|
+
const signature = options.signature ?? defaultWriteSignature();
|
|
28
|
+
return parts("write", signature, options, PI_BUILTIN_WRITE_SNIPPET, WRITE_GUIDELINES);
|
|
29
|
+
}
|
|
30
|
+
export function piApplyPatchParts(options) {
|
|
31
|
+
const signature = options.signature ?? freeformPatchSignature();
|
|
32
|
+
return parts("apply_patch", signature, options, PATCH_SNIPPET, PATCH_GUIDELINES);
|
|
33
|
+
}
|
|
34
|
+
function parts(tool, signature, options, snippet, guidelines) {
|
|
35
|
+
return {
|
|
36
|
+
tool,
|
|
37
|
+
signature,
|
|
38
|
+
promptSnippet: options.promptSnippet ?? snippet,
|
|
39
|
+
promptGuidelines: options.promptGuidelines ?? guidelines,
|
|
40
|
+
messages: { ...writeSignatureMessages(signature), ...options.messages },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a Pi edit tool rooted at each call's ctx.cwd. Throws TypeError when
|
|
45
|
+
* options set fs, cwd, or allowedRoots. Read-before-write is off unless you
|
|
46
|
+
* pass a `state`; use createPiFsTools() for a store shared with read.
|
|
47
|
+
*/
|
|
48
|
+
export function createPiEditTool(options = {}) {
|
|
49
|
+
checkPiOptions(options, "edit");
|
|
50
|
+
const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode, signature: _signature, promptSnippet: _snippet, promptGuidelines: _lines, ...deps } = options;
|
|
51
|
+
const fs = piFileSystems({ denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode });
|
|
52
|
+
const made = piEditParts(options);
|
|
53
|
+
const { messages } = made;
|
|
54
|
+
// digest: null narrows the options to the branch without a state.
|
|
55
|
+
if (deps.digest === null) {
|
|
56
|
+
return adaptPiMutationTool(made, createEditTool({ ...deps, fs, messages, digest: null }));
|
|
57
|
+
}
|
|
58
|
+
const digest = deps.digest ?? nodeDigest();
|
|
59
|
+
return adaptPiMutationTool(made, createEditTool({ ...deps, fs, messages, digest }));
|
|
60
|
+
}
|
|
61
|
+
/** Creates a Pi write tool, as createPiEditTool. `details` is always undefined, as for Pi's own write. */
|
|
62
|
+
export function createPiWriteTool(options = {}) {
|
|
63
|
+
checkPiOptions(options, "write");
|
|
64
|
+
const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode, signature: _signature, promptSnippet: _snippet, promptGuidelines: _lines, ...deps } = options;
|
|
65
|
+
const fs = piFileSystems({ denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode });
|
|
66
|
+
const made = piWriteParts(options);
|
|
67
|
+
const { messages } = made;
|
|
68
|
+
if (deps.digest === null) {
|
|
69
|
+
return adaptPiMutationTool(made, createWriteTool({ ...deps, fs, messages, digest: null }));
|
|
70
|
+
}
|
|
71
|
+
const digest = deps.digest ?? nodeDigest();
|
|
72
|
+
return adaptPiMutationTool(made, createWriteTool({ ...deps, fs, messages, digest }));
|
|
73
|
+
}
|
|
74
|
+
/** Creates a Pi apply_patch tool, as createPiEditTool. A grammar signature sets constrainedSampling. */
|
|
75
|
+
export function createPiApplyPatchTool(options = {}) {
|
|
76
|
+
checkPiOptions(options, "apply_patch");
|
|
77
|
+
const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode, signature: _signature, promptSnippet: _snippet, promptGuidelines: _lines, ...deps } = options;
|
|
78
|
+
const fs = piFileSystems({ denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode });
|
|
79
|
+
const made = piApplyPatchParts(options);
|
|
80
|
+
const { messages } = made;
|
|
81
|
+
if (deps.digest === null) {
|
|
82
|
+
return adaptPiMutationTool(made, createApplyPatchTool({ ...deps, fs, messages, digest: null }));
|
|
83
|
+
}
|
|
84
|
+
const digest = deps.digest ?? nodeDigest();
|
|
85
|
+
return adaptPiMutationTool(made, createApplyPatchTool({ ...deps, fs, messages, digest }));
|
|
86
|
+
}
|
|
87
|
+
export function adaptPiMutationTool(made, tool) {
|
|
88
|
+
const { signature, tool: toolName, promptSnippet, promptGuidelines } = made;
|
|
89
|
+
const withDetails = toolName !== "write";
|
|
90
|
+
const grammar = signature.grammar;
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
name: signature.name,
|
|
93
|
+
label: signature.name,
|
|
94
|
+
description: signature.description,
|
|
95
|
+
promptSnippet,
|
|
96
|
+
promptGuidelines: [...promptGuidelines],
|
|
97
|
+
parameters: Type.Unsafe(signature.schema),
|
|
98
|
+
...(grammar === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
constrainedSampling: { type: "grammar", variants: { openai_lark: grammar.lark } },
|
|
102
|
+
}),
|
|
103
|
+
async execute(toolCallId, input, signal, _onUpdate, ctx) {
|
|
104
|
+
checkPiContext(ctx, toolName);
|
|
105
|
+
const call = {
|
|
106
|
+
...(signal === undefined ? {} : { signal }),
|
|
107
|
+
callId: toolCallId,
|
|
108
|
+
host: ctx,
|
|
109
|
+
};
|
|
110
|
+
const result = await tool(signature.toInput(input), call);
|
|
111
|
+
return {
|
|
112
|
+
content: result.content.map((part) => toPiPart(part, toolName)),
|
|
113
|
+
details: withDetails ? toPiMutationDetails(result) : undefined,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
package/dist/parts.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContentPart } from "@better-fs-tools/read";
|
|
2
|
+
export type PiContentPart = {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function toPiPart(part: ContentPart, tool: string): PiContentPart;
|
package/dist/parts.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
export function toPiPart(part, tool) {
|
|
3
|
+
switch (part.type) {
|
|
4
|
+
case "text":
|
|
5
|
+
return { type: "text", text: part.text };
|
|
6
|
+
case "media":
|
|
7
|
+
// Pi's tool content takes images only.
|
|
8
|
+
if (!part.mediaType.startsWith("image/")) {
|
|
9
|
+
return {
|
|
10
|
+
type: "text",
|
|
11
|
+
text: `[${tool}:media-omitted] ${part.mediaType} (${part.data.byteLength} bytes) cannot be shown in Pi.`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
type: "image",
|
|
16
|
+
data: Buffer.from(part.data.buffer, part.data.byteOffset, part.data.byteLength).toString("base64"),
|
|
17
|
+
mimeType: part.mediaType,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/roots.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NodeFileSystem, NodeFileSystemOptions } from "@better-fs-tools/node";
|
|
2
|
+
import type { ToolCallContext } from "@better-fs-tools/read";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
/** Filesystem options for a Pi tool. The root is always ctx.cwd. */
|
|
5
|
+
export interface PiRootOptions {
|
|
6
|
+
/** Added to /dev, /proc, /sys. */
|
|
7
|
+
readonly denyRoots?: readonly string[];
|
|
8
|
+
readonly symlinks?: NodeFileSystemOptions["symlinks"];
|
|
9
|
+
/** How to replace a file with more than one hard link. Default "refuse". */
|
|
10
|
+
readonly hardLinks?: "refuse" | "in-place";
|
|
11
|
+
/** Mode of a new file, exactly. Default 0o666 less the process umask. */
|
|
12
|
+
readonly newFileMode?: number;
|
|
13
|
+
/** Mode of a directory that createParents makes, exactly. Default 0o777 less the umask. */
|
|
14
|
+
readonly newDirectoryMode?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const PI_ROOT_KEYS: readonly ["denyRoots", "symlinks", "hardLinks", "newFileMode", "newDirectoryMode"];
|
|
17
|
+
export type PiFileSystems = (call: ToolCallContext<ExtensionContext>) => NodeFileSystem;
|
|
18
|
+
export declare function checkPiOptions(options: unknown, tool: string): void;
|
|
19
|
+
export declare function checkPiContext(ctx: unknown, tool: string): void;
|
|
20
|
+
export declare function piFileSystems(options: PiRootOptions): PiFileSystems;
|
package/dist/roots.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { nodeFileSystem } from "@better-fs-tools/node";
|
|
3
|
+
const MAX_CACHED_ROOTS = 8;
|
|
4
|
+
const ROOT_WIDENING_OPTIONS = ["fs", "cwd", "allowedRoots"];
|
|
5
|
+
export const PI_ROOT_KEYS = [
|
|
6
|
+
"denyRoots",
|
|
7
|
+
"symlinks",
|
|
8
|
+
"hardLinks",
|
|
9
|
+
"newFileMode",
|
|
10
|
+
"newDirectoryMode",
|
|
11
|
+
];
|
|
12
|
+
export function checkPiOptions(options, tool) {
|
|
13
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
14
|
+
throw new TypeError(`Pi ${tool} tool options must be an object`);
|
|
15
|
+
}
|
|
16
|
+
for (const key of ROOT_WIDENING_OPTIONS) {
|
|
17
|
+
if (Object.hasOwn(options, key)) {
|
|
18
|
+
throw new TypeError(`Pi ${tool} tool options cannot set ${key}: the root is bound to ctx.cwd on every call`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function checkPiContext(ctx, tool) {
|
|
23
|
+
if (ctx === null ||
|
|
24
|
+
typeof ctx !== "object" ||
|
|
25
|
+
typeof ctx.cwd !== "string" ||
|
|
26
|
+
ctx.cwd.length === 0) {
|
|
27
|
+
throw new TypeError(`Pi ${tool} execution requires ctx.cwd`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function piFileSystems(options) {
|
|
31
|
+
const { denyRoots, symlinks, hardLinks, newFileMode, newDirectoryMode } = options;
|
|
32
|
+
const fileSystemFor = rootCache((root) => nodeFileSystem({
|
|
33
|
+
cwd: root,
|
|
34
|
+
allowedRoots: [root],
|
|
35
|
+
...(denyRoots === undefined ? {} : { denyRoots }),
|
|
36
|
+
...(symlinks === undefined ? {} : { symlinks }),
|
|
37
|
+
...(hardLinks === undefined ? {} : { hardLinks }),
|
|
38
|
+
...(newFileMode === undefined ? {} : { newFileMode }),
|
|
39
|
+
...(newDirectoryMode === undefined ? {} : { newDirectoryMode }),
|
|
40
|
+
}));
|
|
41
|
+
return (call) => fileSystemFor(path.resolve(call.host.cwd));
|
|
42
|
+
}
|
|
43
|
+
function rootCache(build) {
|
|
44
|
+
const cache = new Map();
|
|
45
|
+
return (root) => {
|
|
46
|
+
const cached = cache.get(root);
|
|
47
|
+
if (cached !== undefined)
|
|
48
|
+
return cached;
|
|
49
|
+
const fs = build(root);
|
|
50
|
+
if (cache.size >= MAX_CACHED_ROOTS) {
|
|
51
|
+
const oldest = cache.keys().next();
|
|
52
|
+
if (!oldest.done)
|
|
53
|
+
cache.delete(oldest.value);
|
|
54
|
+
}
|
|
55
|
+
cache.set(root, fs);
|
|
56
|
+
return fs;
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import type { NodeFileSystemOptions } from "@better-fs-tools/node";
|
|
4
|
+
import type { Digest, JsonObject, ReadFormatter, ReadLimits, ReadMessageCatalog, ReadTool, ReadToolDeps, StateNeedsDigestOrDefault } from "@better-fs-tools/read";
|
|
5
|
+
import type { ReadSignature } from "@better-fs-tools/read/signature";
|
|
6
|
+
import type { PiReadDetails } from "./details.ts";
|
|
7
|
+
import type { PiContentPart } from "./parts.ts";
|
|
8
|
+
export type { PiContentPart } from "./parts.ts";
|
|
9
|
+
/** Pi prompt options of the read tool. */
|
|
10
|
+
export interface PiReadOptions {
|
|
11
|
+
/** Default defaultReadSignature({ name: "read" }). */
|
|
12
|
+
readonly signature?: ReadSignature;
|
|
13
|
+
/** Default "Read file contents". */
|
|
14
|
+
readonly promptSnippet?: string;
|
|
15
|
+
/** Default ["Use read to examine files instead of cat or sed."]. */
|
|
16
|
+
readonly promptGuidelines?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for createPiReadTool. digest defaults to nodeDigest(); a state needs
|
|
20
|
+
* a digest that is not null.
|
|
21
|
+
*/
|
|
22
|
+
export type CreatePiReadToolOptions = Omit<ReadToolDeps<ExtensionContext>, "fs"> & StateNeedsDigestOrDefault & PiReadOptions & {
|
|
23
|
+
/** Added to /dev, /proc, /sys. */
|
|
24
|
+
readonly denyRoots?: readonly string[];
|
|
25
|
+
readonly symlinks?: NodeFileSystemOptions["symlinks"];
|
|
26
|
+
};
|
|
27
|
+
export interface PiReadToolResult {
|
|
28
|
+
content: PiContentPart[];
|
|
29
|
+
details: PiReadDetails;
|
|
30
|
+
}
|
|
31
|
+
/** Assignable to ToolDefinition<TSchema, PiReadDetails> from Pi 0.84.2. */
|
|
32
|
+
export interface PiReadTool {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly label: string;
|
|
35
|
+
readonly description: string;
|
|
36
|
+
readonly promptSnippet: string;
|
|
37
|
+
readonly promptGuidelines: string[];
|
|
38
|
+
/** The JSON Schema of the tool input. */
|
|
39
|
+
readonly parameters: TSchema;
|
|
40
|
+
execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiReadToolResult>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates a Pi read tool rooted at each call's ctx.cwd. Throws TypeError when
|
|
44
|
+
* options set fs, cwd, or allowedRoots. It keeps no read state by default and
|
|
45
|
+
* writes nothing outside the process.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createPiReadTool(options?: CreatePiReadToolOptions): PiReadTool;
|
|
48
|
+
export interface PiReadParts {
|
|
49
|
+
readonly signature: ReadSignature;
|
|
50
|
+
readonly promptSnippet: string;
|
|
51
|
+
readonly promptGuidelines: readonly string[];
|
|
52
|
+
readonly limits: Readonly<ReadLimits>;
|
|
53
|
+
readonly formatter: ReadFormatter<ExtensionContext>;
|
|
54
|
+
readonly messages: Partial<ReadMessageCatalog>;
|
|
55
|
+
}
|
|
56
|
+
export declare function piReadParts(options: PiReadOptions & Pick<Partial<ReadToolDeps<ExtensionContext>>, "limits" | "messages" | "formatter">): PiReadParts;
|
|
57
|
+
export declare function piReadDeps(parts: PiReadParts): {
|
|
58
|
+
limits: Readonly<ReadLimits>;
|
|
59
|
+
formatter: ReadFormatter<ExtensionContext>;
|
|
60
|
+
messages: Partial<ReadMessageCatalog>;
|
|
61
|
+
};
|
|
62
|
+
export declare function adaptPiReadTool(parts: PiReadParts, read: ReadTool<ExtensionContext>, digest: Digest | null): PiReadTool;
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { nodeDigest } from "@better-fs-tools/node";
|
|
3
|
+
import { createReadTool, lineNumberFormatter, resolveReadLimits } from "@better-fs-tools/read";
|
|
4
|
+
import { defaultReadSignature, readSignatureMessages } from "@better-fs-tools/read/signature";
|
|
5
|
+
import { toPiReadDetails } from "./details.js";
|
|
6
|
+
import { toPiPart } from "./parts.js";
|
|
7
|
+
import { checkPiContext, checkPiOptions, piFileSystems } from "./roots.js";
|
|
8
|
+
const PI_BUILTIN_READ_SNIPPET = "Read file contents";
|
|
9
|
+
const PI_BUILTIN_READ_GUIDELINES = [
|
|
10
|
+
"Use read to examine files instead of cat or sed.",
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Pi read tool rooted at each call's ctx.cwd. Throws TypeError when
|
|
14
|
+
* options set fs, cwd, or allowedRoots. It keeps no read state by default and
|
|
15
|
+
* writes nothing outside the process.
|
|
16
|
+
*/
|
|
17
|
+
export function createPiReadTool(options = {}) {
|
|
18
|
+
checkPiOptions(options, "read");
|
|
19
|
+
const { denyRoots, symlinks, signature: _signature, promptSnippet: _promptSnippet, promptGuidelines: _promptGuidelines, ...deps } = options;
|
|
20
|
+
const parts = piReadParts(options);
|
|
21
|
+
const fs = piFileSystems({ denyRoots, symlinks });
|
|
22
|
+
// digest: null narrows the options to the branch without a state.
|
|
23
|
+
if (deps.digest === null) {
|
|
24
|
+
const read = createReadTool({ ...deps, ...piReadDeps(parts), fs, digest: null });
|
|
25
|
+
return adaptPiReadTool(parts, read, null);
|
|
26
|
+
}
|
|
27
|
+
const digest = deps.digest ?? nodeDigest();
|
|
28
|
+
const read = createReadTool({ ...deps, ...piReadDeps(parts), fs, digest });
|
|
29
|
+
return adaptPiReadTool(parts, read, digest);
|
|
30
|
+
}
|
|
31
|
+
export function piReadParts(options) {
|
|
32
|
+
const signature = options.signature ?? defaultReadSignature({ name: "read" });
|
|
33
|
+
return {
|
|
34
|
+
signature,
|
|
35
|
+
promptSnippet: options.promptSnippet ?? PI_BUILTIN_READ_SNIPPET,
|
|
36
|
+
promptGuidelines: options.promptGuidelines ?? PI_BUILTIN_READ_GUIDELINES,
|
|
37
|
+
limits: resolveReadLimits(options.limits),
|
|
38
|
+
formatter: options.formatter ?? lineNumberFormatter(),
|
|
39
|
+
messages: { ...readSignatureMessages(signature), ...options.messages },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function piReadDeps(parts) {
|
|
43
|
+
return { limits: parts.limits, formatter: parts.formatter, messages: parts.messages };
|
|
44
|
+
}
|
|
45
|
+
export function adaptPiReadTool(parts, read, digest) {
|
|
46
|
+
const { signature, promptSnippet, promptGuidelines, limits, formatter } = parts;
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
name: signature.name,
|
|
49
|
+
label: signature.name,
|
|
50
|
+
description: signature.description,
|
|
51
|
+
promptSnippet,
|
|
52
|
+
promptGuidelines: [...promptGuidelines],
|
|
53
|
+
parameters: Type.Unsafe(signature.schema),
|
|
54
|
+
async execute(toolCallId, input, signal, _onUpdate, ctx) {
|
|
55
|
+
checkPiContext(ctx, "read");
|
|
56
|
+
const call = {
|
|
57
|
+
...(signal === undefined ? {} : { signal }),
|
|
58
|
+
callId: toolCallId,
|
|
59
|
+
host: ctx,
|
|
60
|
+
};
|
|
61
|
+
const result = await read(signature.toInput(input), call);
|
|
62
|
+
return {
|
|
63
|
+
content: result.content.map((part) => toPiPart(part, "read")),
|
|
64
|
+
details: toPiReadDetails(result, viewOf(formatter, result, { digest, limits, mode: "view", call }), limits.maxViewBytes),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function viewOf(formatter, result, ctx) {
|
|
70
|
+
if (result.status !== "ok")
|
|
71
|
+
return null;
|
|
72
|
+
const { content: _content, ...outcome } = result;
|
|
73
|
+
try {
|
|
74
|
+
const view = formatter.format(outcome, ctx);
|
|
75
|
+
return typeof view === "string" ? view : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// The core already fell back and reported the error in "model" mode.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-fs-tools/pi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Better FS Tools read, edit, write, apply_patch, and bash tools and extension for the Pi coding agent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/gingerhendrix/better-fs-tools.git",
|
|
9
|
+
"directory": "packages/pi"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./extension": {
|
|
22
|
+
"types": "./dist/extension.d.ts",
|
|
23
|
+
"import": "./dist/extension.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./extension": {
|
|
33
|
+
"types": "./dist/extension.d.ts",
|
|
34
|
+
"import": "./dist/extension.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"access": "public",
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./dist/extension.js"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@better-fs-tools/node": "^0.1.0",
|
|
46
|
+
"@better-fs-tools/read": "^0.1.0",
|
|
47
|
+
"@better-fs-tools/shell": "^0.1.0",
|
|
48
|
+
"@better-fs-tools/write": "^0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@better-fs-tools/fs": "^0.1.0",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
53
|
+
"typebox": "^1.3.16"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
57
|
+
"typebox": "^1.3.16"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=24"
|
|
61
|
+
},
|
|
62
|
+
"pi": {
|
|
63
|
+
"extensions": [
|
|
64
|
+
"./dist/extension.js"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
5
67
|
}
|