@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.
- package/README.md +172 -47
- package/dist/agent-core.d.ts +141 -0
- package/dist/agent-core.js +121 -0
- package/dist/bash-tool.d.ts +16 -24
- package/dist/bash-tool.js +23 -58
- package/dist/details.d.ts +7 -1
- package/dist/details.js +18 -0
- package/dist/extension.d.ts +12 -0
- package/dist/extension.js +10 -8
- package/dist/fs-tools.d.ts +70 -36
- package/dist/fs-tools.js +33 -88
- package/dist/index.d.ts +9 -4
- package/dist/index.js +2 -0
- package/dist/locks.d.ts +16 -0
- package/dist/locks.js +68 -0
- package/dist/mutation-tools.d.ts +29 -42
- package/dist/mutation-tools.js +42 -93
- package/dist/node.d.ts +23 -0
- package/dist/node.js +76 -0
- package/dist/parts.js +11 -6
- package/dist/prompt.d.ts +30 -0
- package/dist/prompt.js +35 -0
- package/dist/renderers.d.ts +27 -0
- package/dist/renderers.js +72 -0
- package/dist/results.d.ts +13 -0
- package/dist/results.js +18 -0
- package/dist/tool.d.ts +17 -43
- package/dist/tool.js +26 -75
- package/package.json +25 -8
- package/dist/roots.d.ts +0 -20
- package/dist/roots.js +0 -58
package/dist/mutation-tools.d.ts
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { TSchema } from "typebox";
|
|
3
|
-
import type { JsonObject
|
|
4
|
-
import type {
|
|
5
|
-
import type { EditSignature, MutationSignature, PatchSignature, WriteSignature } from "@better-fs-tools/write/signature";
|
|
3
|
+
import type { JsonObject } from "@better-fs-tools/read";
|
|
4
|
+
import type { ApplyPatchToolDefinitionOptions, EditToolDefinitionOptions, MutationToolDefinition, WriteToolDefinitionOptions } from "@better-fs-tools/write/signature";
|
|
6
5
|
import type { PiMutationDetails } from "./mutation-details.ts";
|
|
7
6
|
import type { PiContentPart } from "./parts.ts";
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readonly signature?: TSignature;
|
|
12
|
-
readonly promptSnippet?: string;
|
|
13
|
-
readonly promptGuidelines?: readonly string[];
|
|
14
|
-
}
|
|
7
|
+
import type { PiPromptOptions } from "./prompt.ts";
|
|
8
|
+
import type { PiToolAnnotations } from "./results.ts";
|
|
9
|
+
type Host = ExtensionContext;
|
|
15
10
|
/**
|
|
16
|
-
* Options for createPiEditTool
|
|
17
|
-
*
|
|
18
|
-
* digest defaults to nodeDigest(); a state needs a digest that is not null.
|
|
11
|
+
* Options for createPiEditTool: the options of editToolDefinition(), with the
|
|
12
|
+
* Pi extension context as host, plus the Pi prompt options. `fs` is required.
|
|
19
13
|
*/
|
|
20
|
-
export type
|
|
21
|
-
/** As
|
|
22
|
-
export type
|
|
23
|
-
/** As
|
|
24
|
-
export type
|
|
14
|
+
export type PiEditToolOptions = EditToolDefinitionOptions<Host> & PiPromptOptions;
|
|
15
|
+
/** As PiEditToolOptions, for write. */
|
|
16
|
+
export type PiWriteToolOptions = WriteToolDefinitionOptions<Host> & PiPromptOptions;
|
|
17
|
+
/** As PiEditToolOptions, for apply_patch. */
|
|
18
|
+
export type PiApplyPatchToolOptions = ApplyPatchToolDefinitionOptions<Host> & PiPromptOptions;
|
|
25
19
|
export interface PiMutationToolResult {
|
|
26
20
|
content: PiContentPart[];
|
|
27
21
|
details: PiMutationDetails | undefined;
|
|
22
|
+
/** true when the change failed (status "error"). Pi then shows the content as an error. */
|
|
23
|
+
isError: boolean;
|
|
28
24
|
}
|
|
29
|
-
/** Assignable to ToolDefinition<TSchema, PiMutationDetails | undefined> from Pi
|
|
25
|
+
/** Assignable to ToolDefinition<TSchema, PiMutationDetails | undefined> from Pi 1.x. */
|
|
30
26
|
export interface PiMutationTool {
|
|
31
27
|
readonly name: string;
|
|
32
28
|
readonly label: string;
|
|
@@ -42,31 +38,22 @@ export interface PiMutationTool {
|
|
|
42
38
|
openai_lark: string;
|
|
43
39
|
};
|
|
44
40
|
};
|
|
41
|
+
/** destructiveHint: true. idempotentHint is true for write only. */
|
|
42
|
+
readonly annotations: PiToolAnnotations;
|
|
45
43
|
execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiMutationToolResult>;
|
|
46
44
|
}
|
|
47
|
-
type
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
45
|
+
export type PiMutationKind = "edit" | "write" | "apply_patch";
|
|
46
|
+
/** Creates a Pi edit tool with the options and defaults of createEditTool(). */
|
|
47
|
+
export declare function createPiEditTool(options: PiEditToolOptions): PiMutationTool;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a Pi write tool with the options and defaults of createWriteTool().
|
|
50
|
+
* `details` is always undefined, as for Pi's own write.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createPiWriteTool(options: PiWriteToolOptions): PiMutationTool;
|
|
61
53
|
/**
|
|
62
|
-
* Creates a Pi
|
|
63
|
-
*
|
|
64
|
-
* pass a `state`; use createPiFsTools({ state }) for a store shared with read.
|
|
54
|
+
* Creates a Pi apply_patch tool with the options and defaults of
|
|
55
|
+
* createApplyPatchTool(). A signature with a grammar sets constrainedSampling.
|
|
65
56
|
*/
|
|
66
|
-
export declare function
|
|
67
|
-
|
|
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;
|
|
57
|
+
export declare function createPiApplyPatchTool(options: PiApplyPatchToolOptions): PiMutationTool;
|
|
58
|
+
export declare function adaptPiMutationTool<TInput>(kind: PiMutationKind, definition: MutationToolDefinition<TInput, Host>, prompt: PiPromptOptions): PiMutationTool;
|
|
72
59
|
export {};
|
package/dist/mutation-tools.js
CHANGED
|
@@ -1,117 +1,66 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
3
|
-
import { createApplyPatchTool, createEditTool, createWriteTool } from "@better-fs-tools/write";
|
|
4
|
-
import { defaultWriteSignature, freeformPatchSignature, multiEditSignature, writeSignatureMessages, } from "@better-fs-tools/write/signature";
|
|
2
|
+
import { applyPatchToolDefinition, editToolDefinition, writeToolDefinition, } from "@better-fs-tools/write/signature";
|
|
5
3
|
import { toPiMutationDetails } from "./mutation-details.js";
|
|
6
4
|
import { toPiPart } from "./parts.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
};
|
|
5
|
+
import { checkPiOptions, piPrompt, splitPrompt } from "./prompt.js";
|
|
6
|
+
import { canonicalArgs, piEditArgs, setRenderArgs } from "./renderers.js";
|
|
7
|
+
import { PI_PATCH_ANNOTATIONS, PI_WRITE_ANNOTATIONS } from "./results.js";
|
|
8
|
+
/** Creates a Pi edit tool with the options and defaults of createEditTool(). */
|
|
9
|
+
export function createPiEditTool(options) {
|
|
10
|
+
checkPiOptions(options, "Pi edit tool");
|
|
11
|
+
const { prompt, rest } = splitPrompt(options);
|
|
12
|
+
return adaptPiMutationTool("edit", editToolDefinition(rest), prompt);
|
|
42
13
|
}
|
|
43
14
|
/**
|
|
44
|
-
* Creates a Pi
|
|
45
|
-
*
|
|
46
|
-
* pass a `state`; use createPiFsTools({ state }) for a store shared with read.
|
|
15
|
+
* Creates a Pi write tool with the options and defaults of createWriteTool().
|
|
16
|
+
* `details` is always undefined, as for Pi's own write.
|
|
47
17
|
*/
|
|
48
|
-
export function
|
|
49
|
-
checkPiOptions(options, "
|
|
50
|
-
const {
|
|
51
|
-
|
|
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 }));
|
|
18
|
+
export function createPiWriteTool(options) {
|
|
19
|
+
checkPiOptions(options, "Pi write tool");
|
|
20
|
+
const { prompt, rest } = splitPrompt(options);
|
|
21
|
+
return adaptPiMutationTool("write", writeToolDefinition(rest), prompt);
|
|
73
22
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const {
|
|
81
|
-
|
|
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 }));
|
|
23
|
+
/**
|
|
24
|
+
* Creates a Pi apply_patch tool with the options and defaults of
|
|
25
|
+
* createApplyPatchTool(). A signature with a grammar sets constrainedSampling.
|
|
26
|
+
*/
|
|
27
|
+
export function createPiApplyPatchTool(options) {
|
|
28
|
+
checkPiOptions(options, "Pi apply_patch tool");
|
|
29
|
+
const { prompt, rest } = splitPrompt(options);
|
|
30
|
+
return adaptPiMutationTool("apply_patch", applyPatchToolDefinition(rest), prompt);
|
|
86
31
|
}
|
|
87
|
-
export function adaptPiMutationTool(
|
|
88
|
-
const {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
promptGuidelines: [...promptGuidelines],
|
|
97
|
-
parameters: Type.Unsafe(signature.schema),
|
|
32
|
+
export function adaptPiMutationTool(kind, definition, prompt) {
|
|
33
|
+
const { grammar, signature } = definition;
|
|
34
|
+
const adapted = Object.freeze({
|
|
35
|
+
name: definition.name,
|
|
36
|
+
label: definition.name,
|
|
37
|
+
description: definition.description,
|
|
38
|
+
...piPrompt(prompt, definition.description),
|
|
39
|
+
parameters: Type.Unsafe(definition.inputSchema),
|
|
40
|
+
annotations: { ...(kind === "write" ? PI_WRITE_ANNOTATIONS : PI_PATCH_ANNOTATIONS) },
|
|
98
41
|
...(grammar === undefined
|
|
99
42
|
? {}
|
|
100
43
|
: {
|
|
101
44
|
constrainedSampling: { type: "grammar", variants: { openai_lark: grammar.lark } },
|
|
102
45
|
}),
|
|
103
46
|
async execute(toolCallId, input, signal, _onUpdate, ctx) {
|
|
104
|
-
checkPiContext(ctx, toolName);
|
|
105
47
|
const call = {
|
|
106
48
|
...(signal === undefined ? {} : { signal }),
|
|
107
49
|
callId: toolCallId,
|
|
108
50
|
host: ctx,
|
|
109
51
|
};
|
|
110
|
-
const result = await
|
|
52
|
+
const result = await definition.execute(input, call);
|
|
111
53
|
return {
|
|
112
|
-
content: result.content.map((part) => toPiPart(part,
|
|
113
|
-
details:
|
|
54
|
+
content: result.content.map((part) => toPiPart(part, kind)),
|
|
55
|
+
details: kind === "write" ? undefined : toPiMutationDetails(result),
|
|
56
|
+
isError: result.status === "error",
|
|
114
57
|
};
|
|
115
58
|
},
|
|
116
59
|
});
|
|
60
|
+
// Pi has built-in renderers for edit and write. apply_patch has none.
|
|
61
|
+
if (kind !== "apply_patch") {
|
|
62
|
+
const toPi = kind === "edit" ? piEditArgs : canonicalArgs;
|
|
63
|
+
setRenderArgs(adapted, (args) => signature.toInput(args), toPi);
|
|
64
|
+
}
|
|
65
|
+
return adapted;
|
|
117
66
|
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { NodeCommandRunnerOptions, NodeFileSystem, NodeFileSystemOptions } from "@better-fs-tools/node";
|
|
3
|
+
import type { ToolCallContext } from "@better-fs-tools/read";
|
|
4
|
+
import type { CommandRunner } from "@better-fs-tools/shell";
|
|
5
|
+
/** The options of nodeFileSystem() without `cwd` and `allowedRoots`: the root is always ctx.cwd. */
|
|
6
|
+
export type PiCwdFileSystemOptions = Omit<NodeFileSystemOptions, "cwd" | "allowedRoots">;
|
|
7
|
+
/** The options of nodeCommandRunner() without `cwd`: commands run in ctx.cwd. */
|
|
8
|
+
export type PiCwdCommandRunnerOptions = Omit<NodeCommandRunnerOptions, "cwd">;
|
|
9
|
+
/** A factory of the call, as the `fs` and `runner` options of a tool take. */
|
|
10
|
+
export type PiCallFactory<T> = (call: ToolCallContext<ExtensionContext>) => T;
|
|
11
|
+
/**
|
|
12
|
+
* A per-call filesystem: nodeFileSystem() with `cwd` and `allowedRoots` set to
|
|
13
|
+
* the call's ctx.cwd. It keeps the filesystems of the last 8 roots. The
|
|
14
|
+
* factory throws TypeError when ctx.cwd is missing or empty, and when
|
|
15
|
+
* `options` sets `cwd` or `allowedRoots`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function piCwdFileSystem(options?: PiCwdFileSystemOptions): PiCallFactory<NodeFileSystem>;
|
|
18
|
+
/**
|
|
19
|
+
* A per-call runner: nodeCommandRunner() at the call's ctx.cwd. It keeps the
|
|
20
|
+
* runners of the last 8 directories used. The factory throws TypeError when
|
|
21
|
+
* ctx.cwd is not an absolute path, and when `options` sets `cwd`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function piCwdCommandRunner(options?: PiCwdCommandRunnerOptions): PiCallFactory<CommandRunner>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./node` subpath: Node backends for a Pi tool, rooted at each call's
|
|
3
|
+
* ctx.cwd. Pass `fs: piCwdFileSystem()` to a Pi file tool, and
|
|
4
|
+
* `runner: piCwdCommandRunner()` to createPiBashTool(). The main entry does
|
|
5
|
+
* not import Node.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { nodeCommandRunner, nodeFileSystem } from "@better-fs-tools/node";
|
|
9
|
+
const MAX_CACHED = 8;
|
|
10
|
+
/**
|
|
11
|
+
* A per-call filesystem: nodeFileSystem() with `cwd` and `allowedRoots` set to
|
|
12
|
+
* the call's ctx.cwd. It keeps the filesystems of the last 8 roots. The
|
|
13
|
+
* factory throws TypeError when ctx.cwd is missing or empty, and when
|
|
14
|
+
* `options` sets `cwd` or `allowedRoots`.
|
|
15
|
+
*/
|
|
16
|
+
export function piCwdFileSystem(options = {}) {
|
|
17
|
+
checkOptions(options, "piCwdFileSystem", ["cwd", "allowedRoots"]);
|
|
18
|
+
const cache = new Map();
|
|
19
|
+
return (call) => {
|
|
20
|
+
const root = path.resolve(cwdOf(call, "piCwdFileSystem"));
|
|
21
|
+
const cached = cache.get(root);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const fs = nodeFileSystem({ ...options, cwd: root, allowedRoots: [root] });
|
|
25
|
+
if (cache.size >= MAX_CACHED) {
|
|
26
|
+
const oldest = cache.keys().next();
|
|
27
|
+
if (!oldest.done)
|
|
28
|
+
cache.delete(oldest.value);
|
|
29
|
+
}
|
|
30
|
+
cache.set(root, fs);
|
|
31
|
+
return fs;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A per-call runner: nodeCommandRunner() at the call's ctx.cwd. It keeps the
|
|
36
|
+
* runners of the last 8 directories used. The factory throws TypeError when
|
|
37
|
+
* ctx.cwd is not an absolute path, and when `options` sets `cwd`.
|
|
38
|
+
*/
|
|
39
|
+
export function piCwdCommandRunner(options = {}) {
|
|
40
|
+
checkOptions(options, "piCwdCommandRunner", ["cwd"]);
|
|
41
|
+
const cache = new Map();
|
|
42
|
+
return (call) => {
|
|
43
|
+
const cwd = cwdOf(call, "piCwdCommandRunner");
|
|
44
|
+
if (!path.isAbsolute(cwd)) {
|
|
45
|
+
throw new TypeError("piCwdCommandRunner needs an absolute ctx.cwd");
|
|
46
|
+
}
|
|
47
|
+
const cached = cache.get(cwd);
|
|
48
|
+
if (cached !== undefined) {
|
|
49
|
+
cache.delete(cwd);
|
|
50
|
+
cache.set(cwd, cached);
|
|
51
|
+
return cached;
|
|
52
|
+
}
|
|
53
|
+
const made = nodeCommandRunner({ ...options, cwd });
|
|
54
|
+
cache.set(cwd, made);
|
|
55
|
+
if (cache.size > MAX_CACHED)
|
|
56
|
+
cache.delete(cache.keys().next().value);
|
|
57
|
+
return made;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function cwdOf(call, label) {
|
|
61
|
+
const cwd = call?.host?.cwd;
|
|
62
|
+
if (typeof cwd !== "string" || cwd === "") {
|
|
63
|
+
throw new TypeError(`${label} needs ctx.cwd: pass the Pi extension context as the host`);
|
|
64
|
+
}
|
|
65
|
+
return cwd;
|
|
66
|
+
}
|
|
67
|
+
function checkOptions(options, label, refused) {
|
|
68
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
69
|
+
throw new TypeError(`${label} options must be an object`);
|
|
70
|
+
}
|
|
71
|
+
for (const key of refused) {
|
|
72
|
+
if (Object.hasOwn(options, key)) {
|
|
73
|
+
throw new TypeError(`${label} options cannot set ${key}: the root is ctx.cwd on every call`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/parts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Keeps each String.fromCharCode spread under engine argument-count limits.
|
|
2
|
+
const CHUNK = 0x8000;
|
|
2
3
|
export function toPiPart(part, tool) {
|
|
3
4
|
switch (part.type) {
|
|
4
5
|
case "text":
|
|
@@ -11,10 +12,14 @@ export function toPiPart(part, tool) {
|
|
|
11
12
|
text: `[${tool}:media-omitted] ${part.mediaType} (${part.data.byteLength} bytes) cannot be shown in Pi.`,
|
|
12
13
|
};
|
|
13
14
|
}
|
|
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
|
-
};
|
|
15
|
+
return { type: "image", data: toBase64(part.data), mimeType: part.mediaType };
|
|
19
16
|
}
|
|
20
17
|
}
|
|
18
|
+
/** Base64 without node:buffer, so the agent core tools run in a browser or a Worker. */
|
|
19
|
+
function toBase64(bytes) {
|
|
20
|
+
let binary = "";
|
|
21
|
+
for (let offset = 0; offset < bytes.byteLength; offset += CHUNK) {
|
|
22
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + CHUNK));
|
|
23
|
+
}
|
|
24
|
+
return btoa(binary);
|
|
25
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Pi prompt options of every Pi tool. */
|
|
2
|
+
export interface PiPromptOptions {
|
|
3
|
+
/** Default the first sentence of the signature description. Pi lists the tool with it. */
|
|
4
|
+
readonly promptSnippet?: string;
|
|
5
|
+
/** Default []. */
|
|
6
|
+
readonly promptGuidelines?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** The prompt options of a Pi tool, with their defaults applied. */
|
|
9
|
+
export interface PiPrompt {
|
|
10
|
+
readonly promptSnippet: string;
|
|
11
|
+
readonly promptGuidelines: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Applies the defaults of the prompt options for a tool with this description. */
|
|
14
|
+
export declare function piPrompt(options: PiPromptOptions, description: string): PiPrompt;
|
|
15
|
+
/** Omit for each member of a union, so the state and digest pairing stays. */
|
|
16
|
+
type WithoutPrompt<T> = T extends unknown ? Omit<T, keyof PiPromptOptions> : never;
|
|
17
|
+
/** Splits the prompt options off a Pi tool's options. */
|
|
18
|
+
export declare function splitPrompt<T extends PiPromptOptions>(options: T): {
|
|
19
|
+
readonly prompt: PiPromptOptions;
|
|
20
|
+
readonly rest: WithoutPrompt<T>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The default Pi promptSnippet of a tool: the first sentence of its signature
|
|
24
|
+
* description, without the full stop. Pi lists a custom tool under "Available
|
|
25
|
+
* tools" only when it has a snippet, so every Pi tool gets one. The text comes
|
|
26
|
+
* from the shared signature, so each integration describes the tool the same way.
|
|
27
|
+
*/
|
|
28
|
+
export declare function signatureSnippet(description: string): string;
|
|
29
|
+
export declare function checkPiOptions(options: unknown, label: string): void;
|
|
30
|
+
export {};
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Applies the defaults of the prompt options for a tool with this description. */
|
|
2
|
+
export function piPrompt(options, description) {
|
|
3
|
+
return {
|
|
4
|
+
promptSnippet: options.promptSnippet ?? signatureSnippet(description),
|
|
5
|
+
promptGuidelines: [...(options.promptGuidelines ?? [])],
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Splits the prompt options off a Pi tool's options. */
|
|
9
|
+
export function splitPrompt(options) {
|
|
10
|
+
const { promptSnippet, promptGuidelines, ...rest } = options;
|
|
11
|
+
return {
|
|
12
|
+
prompt: {
|
|
13
|
+
...(promptSnippet === undefined ? {} : { promptSnippet }),
|
|
14
|
+
...(promptGuidelines === undefined ? {} : { promptGuidelines }),
|
|
15
|
+
},
|
|
16
|
+
rest: rest,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The default Pi promptSnippet of a tool: the first sentence of its signature
|
|
21
|
+
* description, without the full stop. Pi lists a custom tool under "Available
|
|
22
|
+
* tools" only when it has a snippet, so every Pi tool gets one. The text comes
|
|
23
|
+
* from the shared signature, so each integration describes the tool the same way.
|
|
24
|
+
*/
|
|
25
|
+
export function signatureSnippet(description) {
|
|
26
|
+
const text = description.trim();
|
|
27
|
+
const end = /\.(\s|$)/u.exec(text);
|
|
28
|
+
const sentence = end === null ? text.split("\n", 1)[0] : text.slice(0, end.index);
|
|
29
|
+
return sentence.trim();
|
|
30
|
+
}
|
|
31
|
+
export function checkPiOptions(options, label) {
|
|
32
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
33
|
+
throw new TypeError(`${label} options must be an object`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolRendererResolver } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/** Turns the model's arguments into the arguments that Pi's built-in renderer of the tool reads. */
|
|
3
|
+
type RenderArgs = (args: unknown) => unknown;
|
|
4
|
+
/** Records how a tool's arguments map to the arguments of Pi's built-in renderer of the same name. */
|
|
5
|
+
export declare function setRenderArgs(tool: object, toInput: (args: unknown) => unknown, toPi: RenderArgs): void;
|
|
6
|
+
/** Pi's read and write renderers read path, offset, limit, and content: the canonical names. */
|
|
7
|
+
export declare function canonicalArgs(input: unknown): unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Pi's edit renderer reads { path, edits: [{ oldText, newText }] }, the
|
|
10
|
+
* canonical edit input. It cannot show a replace-all edit, so then it gets
|
|
11
|
+
* only the path and shows the diff of the result.
|
|
12
|
+
*/
|
|
13
|
+
export declare function piEditArgs(input: unknown): unknown;
|
|
14
|
+
/** Pi's bash renderer shows `timeout` in seconds. */
|
|
15
|
+
export declare function piBashArgs(input: unknown): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* A Pi tool renderer resolver for Better FS Tools. A tool named read, edit,
|
|
18
|
+
* write, or bash gets Pi's built-in renderer of that name. That renderer reads
|
|
19
|
+
* Pi's own argument names. This resolver gives it the arguments in those names,
|
|
20
|
+
* so it draws the call for any signature: the edit preview, the read range, and
|
|
21
|
+
* the bash timeout. Register it with pi.registerToolRenderer(). The extension
|
|
22
|
+
* entry does this for its own tools.
|
|
23
|
+
*/
|
|
24
|
+
export declare function piToolRenderers(tools: Iterable<{
|
|
25
|
+
readonly name: string;
|
|
26
|
+
}>): ToolRendererResolver;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const RENDER_ARGS = new WeakMap();
|
|
2
|
+
/** Records how a tool's arguments map to the arguments of Pi's built-in renderer of the same name. */
|
|
3
|
+
export function setRenderArgs(tool, toInput, toPi) {
|
|
4
|
+
RENDER_ARGS.set(tool, (args) => {
|
|
5
|
+
try {
|
|
6
|
+
return toPi(toInput(args));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Arguments that stream in, or that are not valid, go to the renderer as they are.
|
|
10
|
+
return args;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Pi's read and write renderers read path, offset, limit, and content: the canonical names. */
|
|
15
|
+
export function canonicalArgs(input) {
|
|
16
|
+
return input;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pi's edit renderer reads { path, edits: [{ oldText, newText }] }, the
|
|
20
|
+
* canonical edit input. It cannot show a replace-all edit, so then it gets
|
|
21
|
+
* only the path and shows the diff of the result.
|
|
22
|
+
*/
|
|
23
|
+
export function piEditArgs(input) {
|
|
24
|
+
const { path, edits } = input;
|
|
25
|
+
if (edits.some((edit) => edit.replaceAll === true))
|
|
26
|
+
return { path };
|
|
27
|
+
return { path, edits: edits.map(({ oldText, newText }) => ({ oldText, newText })) };
|
|
28
|
+
}
|
|
29
|
+
/** Pi's bash renderer shows `timeout` in seconds. */
|
|
30
|
+
export function piBashArgs(input) {
|
|
31
|
+
const { command, timeoutMs } = input;
|
|
32
|
+
return timeoutMs === undefined ? { command } : { command, timeout: timeoutMs / 1_000 };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A Pi tool renderer resolver for Better FS Tools. A tool named read, edit,
|
|
36
|
+
* write, or bash gets Pi's built-in renderer of that name. That renderer reads
|
|
37
|
+
* Pi's own argument names. This resolver gives it the arguments in those names,
|
|
38
|
+
* so it draws the call for any signature: the edit preview, the read range, and
|
|
39
|
+
* the bash timeout. Register it with pi.registerToolRenderer(). The extension
|
|
40
|
+
* entry does this for its own tools.
|
|
41
|
+
*/
|
|
42
|
+
export function piToolRenderers(tools) {
|
|
43
|
+
const byName = new Map();
|
|
44
|
+
for (const tool of tools) {
|
|
45
|
+
const renderArgs = RENDER_ARGS.get(tool);
|
|
46
|
+
if (renderArgs !== undefined)
|
|
47
|
+
byName.set(tool.name, renderArgs);
|
|
48
|
+
}
|
|
49
|
+
return (toolName, next) => {
|
|
50
|
+
const base = next();
|
|
51
|
+
const renderArgs = byName.get(toolName);
|
|
52
|
+
if (base === undefined || renderArgs === undefined)
|
|
53
|
+
return base;
|
|
54
|
+
return withArgs(base, renderArgs);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function withArgs(base, renderArgs) {
|
|
58
|
+
const { renderCall, renderResult } = base;
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
...(renderCall === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
renderCall: (args, theme, context) => renderCall(renderArgs(args), theme, { ...context, args: renderArgs(context.args) }),
|
|
65
|
+
}),
|
|
66
|
+
...(renderResult === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: {
|
|
69
|
+
renderResult: (result, options, theme, context) => renderResult(result, options, theme, { ...context, args: renderArgs(context.args) }),
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Pi's ToolAnnotations: MCP tool hints that permission extensions read. */
|
|
2
|
+
export interface PiToolAnnotations {
|
|
3
|
+
readOnlyHint?: boolean;
|
|
4
|
+
destructiveHint?: boolean;
|
|
5
|
+
idempotentHint?: boolean;
|
|
6
|
+
openWorldHint?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** read changes nothing. */
|
|
9
|
+
export declare const PI_READ_ANNOTATIONS: Readonly<PiToolAnnotations>;
|
|
10
|
+
/** edit and apply_patch can replace or remove file content, and a second run is not a no-op. */
|
|
11
|
+
export declare const PI_PATCH_ANNOTATIONS: Readonly<PiToolAnnotations>;
|
|
12
|
+
/** write replaces a whole file. The same call twice gives the same file. */
|
|
13
|
+
export declare const PI_WRITE_ANNOTATIONS: Readonly<PiToolAnnotations>;
|
package/dist/results.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** read changes nothing. */
|
|
2
|
+
export const PI_READ_ANNOTATIONS = Object.freeze({
|
|
3
|
+
readOnlyHint: true,
|
|
4
|
+
});
|
|
5
|
+
/** edit and apply_patch can replace or remove file content, and a second run is not a no-op. */
|
|
6
|
+
export const PI_PATCH_ANNOTATIONS = Object.freeze({
|
|
7
|
+
readOnlyHint: false,
|
|
8
|
+
destructiveHint: true,
|
|
9
|
+
idempotentHint: false,
|
|
10
|
+
openWorldHint: false,
|
|
11
|
+
});
|
|
12
|
+
/** write replaces a whole file. The same call twice gives the same file. */
|
|
13
|
+
export const PI_WRITE_ANNOTATIONS = Object.freeze({
|
|
14
|
+
readOnlyHint: false,
|
|
15
|
+
destructiveHint: true,
|
|
16
|
+
idempotentHint: true,
|
|
17
|
+
openWorldHint: false,
|
|
18
|
+
});
|