@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/dist/tool.d.ts CHANGED
@@ -1,35 +1,25 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { TSchema } from "typebox";
3
- import type { NodeFileSystemOptions } from "@better-fs-tools/node";
4
- import type { Converter, Digest, JsonObject, ReadFormatter, ReadLimits, ReadMessageCatalog, ReadTool, ReadToolDeps, StateNeedsDigestOrDefault } from "@better-fs-tools/read";
5
- import type { ReadSignature } from "@better-fs-tools/read/signature";
3
+ import type { JsonObject } from "@better-fs-tools/read";
4
+ import type { ReadToolDefinition, ReadToolDefinitionOptions } from "@better-fs-tools/read/signature";
6
5
  import type { PiReadDetails } from "./details.ts";
7
6
  import type { PiContentPart } from "./parts.ts";
7
+ import type { PiPromptOptions } from "./prompt.ts";
8
+ import type { PiToolAnnotations } from "./results.ts";
8
9
  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
10
  /**
19
- * Options for createPiReadTool. digest defaults to nodeDigest(); a state needs
20
- * a digest that is not null. converters defaults to [imageConverter()], as
21
- * Pi's own read shows images. A list you pass replaces it: [] turns images off.
11
+ * Options for createPiReadTool: the options of readToolDefinition(), with the
12
+ * Pi extension context as host, plus the Pi prompt options. `fs` is required:
13
+ * piCwdFileSystem() from `./node` roots it at ctx.cwd.
22
14
  */
23
- export type CreatePiReadToolOptions = Omit<ReadToolDeps<ExtensionContext>, "fs"> & StateNeedsDigestOrDefault & PiReadOptions & {
24
- /** Added to /dev, /proc, /sys. */
25
- readonly denyRoots?: readonly string[];
26
- readonly symlinks?: NodeFileSystemOptions["symlinks"];
27
- };
15
+ export type PiReadToolOptions = ReadToolDefinitionOptions<ExtensionContext> & PiPromptOptions;
28
16
  export interface PiReadToolResult {
29
17
  content: PiContentPart[];
30
18
  details: PiReadDetails;
19
+ /** true when the read failed (status "error"). Pi then shows the content as an error. */
20
+ isError: boolean;
31
21
  }
32
- /** Assignable to ToolDefinition<TSchema, PiReadDetails> from Pi 0.84.2. */
22
+ /** Assignable to ToolDefinition<TSchema, PiReadDetails> from Pi 1.x. */
33
23
  export interface PiReadTool {
34
24
  readonly name: string;
35
25
  readonly label: string;
@@ -38,29 +28,13 @@ export interface PiReadTool {
38
28
  readonly promptGuidelines: string[];
39
29
  /** The JSON Schema of the tool input. */
40
30
  readonly parameters: TSchema;
31
+ /** readOnlyHint: true. */
32
+ readonly annotations: PiToolAnnotations;
41
33
  execute(toolCallId: string, input: JsonObject, signal: AbortSignal | undefined, onUpdate: unknown, ctx: ExtensionContext): Promise<PiReadToolResult>;
42
34
  }
43
35
  /**
44
- * Creates a Pi read tool rooted at each call's ctx.cwd. Throws TypeError when
45
- * options set fs, cwd, or allowedRoots. It keeps no read state by default and
46
- * writes nothing outside the process. An image becomes a Pi image part unless
47
- * options.converters leaves imageConverter() out.
36
+ * Creates a Pi read tool with the options and defaults of createReadTool().
37
+ * The call's host is the Pi extension context.
48
38
  */
49
- export declare function createPiReadTool(options?: CreatePiReadToolOptions): PiReadTool;
50
- export interface PiReadParts {
51
- readonly signature: ReadSignature;
52
- readonly promptSnippet: string;
53
- readonly promptGuidelines: readonly string[];
54
- readonly limits: Readonly<ReadLimits>;
55
- readonly formatter: ReadFormatter<ExtensionContext>;
56
- readonly messages: Partial<ReadMessageCatalog>;
57
- readonly converters: readonly Converter<ExtensionContext>[];
58
- }
59
- export declare function piReadParts(options: PiReadOptions & Pick<Partial<ReadToolDeps<ExtensionContext>>, "limits" | "messages" | "formatter" | "converters">): PiReadParts;
60
- export declare function piReadDeps(parts: PiReadParts): {
61
- limits: Readonly<ReadLimits>;
62
- formatter: ReadFormatter<ExtensionContext>;
63
- messages: Partial<ReadMessageCatalog>;
64
- converters: readonly Converter<ExtensionContext>[];
65
- };
66
- export declare function adaptPiReadTool(parts: PiReadParts, read: ReadTool<ExtensionContext>, digest: Digest | null): PiReadTool;
39
+ export declare function createPiReadTool(options: PiReadToolOptions): PiReadTool;
40
+ export declare function adaptPiReadTool(definition: ReadToolDefinition<ExtensionContext>, prompt: PiPromptOptions): PiReadTool;
package/dist/tool.js CHANGED
@@ -1,89 +1,40 @@
1
1
  import { Type } from "typebox";
2
- import { nodeDigest } from "@better-fs-tools/node";
3
- import { createReadTool, imageConverter, lineNumberFormatter, resolveReadLimits, } from "@better-fs-tools/read";
4
- import { defaultReadSignature, readSignatureMessages } from "@better-fs-tools/read/signature";
5
- import { toPiReadDetails } from "./details.js";
2
+ import { readToolDefinition } from "@better-fs-tools/read/signature";
3
+ import { toPiReadDetails, viewOf } from "./details.js";
6
4
  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
- ];
5
+ import { checkPiOptions, piPrompt, splitPrompt } from "./prompt.js";
6
+ import { canonicalArgs, setRenderArgs } from "./renderers.js";
7
+ import { PI_READ_ANNOTATIONS } from "./results.js";
12
8
  /**
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. An image becomes a Pi image part unless
16
- * options.converters leaves imageConverter() out.
9
+ * Creates a Pi read tool with the options and defaults of createReadTool().
10
+ * The call's host is the Pi extension context.
17
11
  */
18
- export function createPiReadTool(options = {}) {
19
- checkPiOptions(options, "read");
20
- const { denyRoots, symlinks, signature: _signature, promptSnippet: _promptSnippet, promptGuidelines: _promptGuidelines, ...deps } = options;
21
- const parts = piReadParts(options);
22
- const fs = piFileSystems({ denyRoots, symlinks });
23
- // digest: null narrows the options to the branch without a state.
24
- if (deps.digest === null) {
25
- const read = createReadTool({ ...deps, ...piReadDeps(parts), fs, digest: null });
26
- return adaptPiReadTool(parts, read, null);
27
- }
28
- const digest = deps.digest ?? nodeDigest();
29
- const read = createReadTool({ ...deps, ...piReadDeps(parts), fs, digest });
30
- return adaptPiReadTool(parts, read, digest);
12
+ export function createPiReadTool(options) {
13
+ checkPiOptions(options, "Pi read tool");
14
+ const { prompt, rest } = splitPrompt(options);
15
+ return adaptPiReadTool(readToolDefinition(rest), prompt);
31
16
  }
32
- export function piReadParts(options) {
33
- const signature = options.signature ?? defaultReadSignature({ name: "read" });
34
- return {
35
- signature,
36
- promptSnippet: options.promptSnippet ?? PI_BUILTIN_READ_SNIPPET,
37
- promptGuidelines: options.promptGuidelines ?? PI_BUILTIN_READ_GUIDELINES,
38
- limits: resolveReadLimits(options.limits),
39
- formatter: options.formatter ?? lineNumberFormatter(),
40
- messages: { ...readSignatureMessages(signature), ...options.messages },
41
- // Pi's own read returns images, so the Pi host turns them on.
42
- converters: options.converters ?? [imageConverter()],
43
- };
44
- }
45
- export function piReadDeps(parts) {
46
- return {
47
- limits: parts.limits,
48
- formatter: parts.formatter,
49
- messages: parts.messages,
50
- converters: parts.converters,
51
- };
52
- }
53
- export function adaptPiReadTool(parts, read, digest) {
54
- const { signature, promptSnippet, promptGuidelines, limits, formatter } = parts;
55
- return Object.freeze({
56
- name: signature.name,
57
- label: signature.name,
58
- description: signature.description,
59
- promptSnippet,
60
- promptGuidelines: [...promptGuidelines],
61
- parameters: Type.Unsafe(signature.schema),
17
+ export function adaptPiReadTool(definition, prompt) {
18
+ const { signature, limits, formatter, digest } = definition;
19
+ const tool = Object.freeze({
20
+ name: definition.name,
21
+ label: definition.name,
22
+ description: definition.description,
23
+ ...piPrompt(prompt, definition.description),
24
+ parameters: Type.Unsafe(definition.inputSchema),
25
+ annotations: { ...PI_READ_ANNOTATIONS },
62
26
  async execute(toolCallId, input, signal, _onUpdate, ctx) {
63
- checkPiContext(ctx, "read");
64
27
  const call = {
65
28
  ...(signal === undefined ? {} : { signal }),
66
29
  callId: toolCallId,
67
30
  host: ctx,
68
31
  };
69
- const result = await read(signature.toInput(input), call);
70
- return {
71
- content: result.content.map((part) => toPiPart(part, "read")),
72
- details: toPiReadDetails(result, viewOf(formatter, result, { digest, limits, mode: "view", call }), limits.maxViewBytes),
73
- };
32
+ const result = await definition.execute(input, call);
33
+ const content = result.content.map((part) => toPiPart(part, "read"));
34
+ const details = toPiReadDetails(result, viewOf(formatter, result, { digest, limits, mode: "view", call }), limits.maxViewBytes);
35
+ return { content, details, isError: result.status === "error" };
74
36
  },
75
37
  });
76
- }
77
- function viewOf(formatter, result, ctx) {
78
- if (result.status !== "ok")
79
- return null;
80
- const { content: _content, ...outcome } = result;
81
- try {
82
- const view = formatter.format(outcome, ctx);
83
- return typeof view === "string" ? view : null;
84
- }
85
- catch {
86
- // The core already fell back and reported the error in "model" mode.
87
- return null;
88
- }
38
+ setRenderArgs(tool, (args) => signature.toInput(args), canonicalArgs);
39
+ return tool;
89
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-fs-tools/pi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Better FS Tools read, edit, write, apply_patch, and bash tools and extension for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,9 +18,17 @@
18
18
  "types": "./dist/index.d.ts",
19
19
  "import": "./dist/index.js"
20
20
  },
21
+ "./agent-core": {
22
+ "types": "./dist/agent-core.d.ts",
23
+ "import": "./dist/agent-core.js"
24
+ },
21
25
  "./extension": {
22
26
  "types": "./dist/extension.d.ts",
23
27
  "import": "./dist/extension.js"
28
+ },
29
+ "./node": {
30
+ "types": "./dist/node.d.ts",
31
+ "import": "./dist/node.js"
24
32
  }
25
33
  },
26
34
  "publishConfig": {
@@ -29,9 +37,17 @@
29
37
  "types": "./dist/index.d.ts",
30
38
  "import": "./dist/index.js"
31
39
  },
40
+ "./agent-core": {
41
+ "types": "./dist/agent-core.d.ts",
42
+ "import": "./dist/agent-core.js"
43
+ },
32
44
  "./extension": {
33
45
  "types": "./dist/extension.d.ts",
34
46
  "import": "./dist/extension.js"
47
+ },
48
+ "./node": {
49
+ "types": "./dist/node.d.ts",
50
+ "import": "./dist/node.js"
35
51
  }
36
52
  },
37
53
  "access": "public",
@@ -42,18 +58,19 @@
42
58
  }
43
59
  },
44
60
  "dependencies": {
45
- "@better-fs-tools/node": "^0.2.0",
46
- "@better-fs-tools/read": "^0.2.0",
47
- "@better-fs-tools/shell": "^0.2.0",
48
- "@better-fs-tools/write": "^0.2.0"
61
+ "@better-fs-tools/node": "^0.3.0",
62
+ "@better-fs-tools/read": "^0.3.0",
63
+ "@better-fs-tools/shell": "^0.3.0",
64
+ "@better-fs-tools/write": "^0.3.0"
49
65
  },
50
66
  "devDependencies": {
51
- "@better-fs-tools/fs": "^0.2.0",
52
- "@earendil-works/pi-coding-agent": "^0.84.2",
67
+ "@better-fs-tools/fs": "^0.3.0",
68
+ "@earendil-works/pi-agent-core": "^1.0.0",
69
+ "@earendil-works/pi-coding-agent": "^1.0.0",
53
70
  "typebox": "^1.3.16"
54
71
  },
55
72
  "peerDependencies": {
56
- "@earendil-works/pi-coding-agent": "^0.84.2",
73
+ "@earendil-works/pi-coding-agent": "^1.0.0",
57
74
  "typebox": "^1.3.16"
58
75
  },
59
76
  "engines": {
package/dist/roots.d.ts DELETED
@@ -1,20 +0,0 @@
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 "in-place". "refuse" fails with DENIED. */
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 DELETED
@@ -1,58 +0,0 @@
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
- }