@aliou/pi-dev-kit 0.6.3 → 0.6.5

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.
@@ -16,15 +16,27 @@ import type {
16
16
  Theme,
17
17
  ToolRenderResultOptions,
18
18
  } from "@mariozechner/pi-coding-agent";
19
- import { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
19
+ import { defineTool, getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
20
20
  import { Container, Markdown, Text } from "@mariozechner/pi-tui";
21
- import { type Static, Type } from "@sinclair/typebox";
21
+ import { type Static, Type } from "typebox";
22
22
  ```
23
23
 
24
24
  ## Registration
25
25
 
26
+ Tool files are extension entry points. Put the tool registration in `src/tools/index.ts`, export a default function, and list that file in `package.json` `pi.extensions`.
27
+
26
28
  ```typescript
27
- const myTool = {
29
+ const parameters = Type.Object({
30
+ query: Type.String({ description: "Search query" }),
31
+ limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
32
+ });
33
+
34
+ type MyToolParams = Static<typeof parameters>;
35
+ interface MyToolDetails {
36
+ results: string[];
37
+ }
38
+
39
+ const myTool = defineTool({
28
40
  name: "my_tool",
29
41
  label: "My Tool", // Required: human-readable name for UI
30
42
  description: "What this tool does. The LLM reads this to decide when to call it.",
@@ -33,17 +45,14 @@ const myTool = {
33
45
  "Use my_tool when the user asks about search.",
34
46
  "Prefer specific queries over broad ones when calling my_tool.",
35
47
  ],
36
- parameters: Type.Object({
37
- query: Type.String({ description: "Search query" }),
38
- limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
39
- }),
48
+ parameters,
40
49
 
41
50
  async execute(
42
- toolCallId: string,
43
- params: MyToolParams,
44
- signal: AbortSignal | undefined,
45
- onUpdate: AgentToolUpdateCallback<MyToolDetails> | undefined,
46
- ctx: ExtensionContext,
51
+ toolCallId,
52
+ params,
53
+ signal,
54
+ onUpdate,
55
+ ctx,
47
56
  ): Promise<AgentToolResult<MyToolDetails>> {
48
57
  const results = await doSomething(params.query, params.limit);
49
58
  return {
@@ -51,15 +60,13 @@ const myTool = {
51
60
  details: { results },
52
61
  };
53
62
  },
54
- };
63
+ });
55
64
 
56
- // Typed param alias - define once, use everywhere
57
- type MyToolParams = Static<typeof myTool.parameters>;
58
- interface MyToolDetails {
59
- results: string[];
60
- }
65
+ export default async function (pi: ExtensionAPI) {
66
+ await configLoader.load();
67
+ const config = configLoader.getConfig();
68
+ if (!config.enabled) return;
61
69
 
62
- export default function (pi: ExtensionAPI) {
63
70
  pi.registerTool(myTool);
64
71
  }
65
72
  ```
@@ -78,7 +85,11 @@ export default function (pi: ExtensionAPI) {
78
85
  | `renderCall` | `function` | No | Custom call rendering |
79
86
  | `renderResult` | `function` | No | Custom result rendering |
80
87
 
81
- ## Typed Param Alias
88
+ ## `defineTool()` and Typed Param Alias
89
+
90
+ Use `defineTool()` for standalone tool definitions. It infers parameter types from the `parameters` field and preserves them for `execute`, `renderCall`, and `renderResult` so you do not need casts or explicit generic arguments at registration/rendering boundaries.
91
+
92
+ Inside the `defineTool({...})` object, do not annotate callback parameter types unless TypeScript actually needs help. The contextual type from Pi already provides `toolCallId`, `params`, `signal`, `onUpdate`, `ctx`, `options`, and `theme`. If you want `renderResult` to see a specific `details` shape, annotate the `execute` return type as `Promise<AgentToolResult<MyToolDetails>>`; that is the useful annotation.
82
93
 
83
94
  Define a type alias at the top of your file instead of repeating `Static<typeof parameters>`:
84
95
 
@@ -89,21 +100,29 @@ const parameters = Type.Object({
89
100
  });
90
101
 
91
102
  type MyToolParams = Static<typeof parameters>;
92
- // Use MyToolParams everywhere: execute params, renderCall args, context.args, etc.
103
+ // Use MyToolParams in helper functions and exported action APIs. Inside defineTool callbacks, prefer inference.
93
104
  ```
94
105
 
95
106
  ## Execute Signature
96
107
 
108
+ ```typescript
109
+ execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>
110
+ ```
111
+
112
+ Pi's `ToolDefinition` type is:
113
+
97
114
  ```typescript
98
115
  execute(
99
116
  toolCallId: string,
100
- params: Static<TParams>, // Typed from the parameters schema
117
+ params: Static<TParams>,
101
118
  signal: AbortSignal | undefined,
102
119
  onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
103
120
  ctx: ExtensionContext,
104
121
  ): Promise<AgentToolResult<TDetails>>
105
122
  ```
106
123
 
124
+ You usually do not need to write those parameter types yourself inside `defineTool()`.
125
+
107
126
  **Parameter order matters.** The signal comes before onUpdate.
108
127
 
109
128
  Always use optional chaining when calling `onUpdate`:
@@ -126,11 +145,13 @@ Prompt metadata is not inherited automatically when you override a built-in tool
126
145
  return {
127
146
  content: (TextContent | ImageContent)[], // Content blocks sent to the LLM
128
147
  details?: TDetails, // Arbitrary data available in the renderer
148
+ terminate?: boolean, // Optional: skip follow-up LLM call when all finalized results in the batch terminate
129
149
  };
130
150
  ```
131
151
 
132
152
  - `content` is what the LLM sees. Each block is `{ type: "text", text: "..." }` or an image. Keep it structured and concise.
133
153
  - `details` is what the renderer sees. Put rich data here for custom display.
154
+ - `terminate: true` is for final/structured-output tools that should end the turn without an automatic follow-up LLM call. It only applies when every finalized tool result in the current batch also returns `terminate: true`.
134
155
 
135
156
  Common pattern:
136
157
 
@@ -221,10 +242,10 @@ Both approaches work. Approach 1 is more common in published extensions. Approac
221
242
 
222
243
  ## Parameters Schema
223
244
 
224
- Use TypeBox (`Type.*`) for parameter schemas. The LLM sees the schema to know what arguments to provide.
245
+ Use TypeBox 1.x (`Type.*`) from `typebox` for parameter schemas. The LLM sees the schema to know what arguments to provide. Do not import from `@sinclair/typebox` in new extensions.
225
246
 
226
247
  ```typescript
227
- import { Type } from "@sinclair/typebox";
248
+ import { Type } from "typebox";
228
249
 
229
250
  // Required string
230
251
  Type.String({ description: "File path to read" })
@@ -838,7 +859,7 @@ import type {
838
859
  } from "@mariozechner/pi-coding-agent";
839
860
  import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
840
861
  import { Container, Text } from "@mariozechner/pi-tui";
841
- import { type Static, Type } from "@sinclair/typebox";
862
+ import { type Static, Type } from "typebox";
842
863
 
843
864
  // Schema
844
865
  const parameters = Type.Object({
@@ -4,13 +4,11 @@ import { ToolBody, ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
4
4
  import type {
5
5
  AgentToolResult,
6
6
  ExtensionAPI,
7
- ExtensionContext,
8
7
  Theme,
9
- ToolRenderResultOptions,
10
8
  } from "@mariozechner/pi-coding-agent";
11
- import { keyHint, VERSION } from "@mariozechner/pi-coding-agent";
9
+ import { defineTool, keyHint, VERSION } from "@mariozechner/pi-coding-agent";
12
10
  import { Text } from "@mariozechner/pi-tui";
13
- import { type Static, Type } from "@sinclair/typebox";
11
+ import { type Static, Type } from "typebox";
14
12
  import { findPiInstallation } from "./utils";
15
13
 
16
14
  const GITHUB_RAW_CHANGELOG_URL =
@@ -32,7 +30,7 @@ const ChangelogParamsSchema = Type.Object({
32
30
  type ChangelogParams = Static<typeof ChangelogParamsSchema>;
33
31
 
34
32
  const ChangelogVersionsParamsSchema = Type.Object({});
35
- type ChangelogVersionsParams = Record<string, never>;
33
+ type ChangelogVersionsParams = Static<typeof ChangelogVersionsParamsSchema>;
36
34
 
37
35
  // ---------------------------------------------------------------------------
38
36
  // Types
@@ -200,6 +198,23 @@ const COLLAPSED_LINES = 8;
200
198
  // Render helpers
201
199
  // ---------------------------------------------------------------------------
202
200
 
201
+ function renderChangelogCall(args: ChangelogParams, theme: Theme) {
202
+ return new ToolCallHeader(
203
+ {
204
+ toolName: "Pi Changelog",
205
+ mainArg: args.version ? `v${args.version}` : "latest",
206
+ },
207
+ theme,
208
+ );
209
+ }
210
+
211
+ function renderChangelogVersionsCall(
212
+ _args: ChangelogVersionsParams,
213
+ theme: Theme,
214
+ ) {
215
+ return new ToolCallHeader({ toolName: "Pi Changelog Versions" }, theme);
216
+ }
217
+
203
218
  function renderChangelogContent(
204
219
  content: string,
205
220
  theme: Theme,
@@ -235,250 +250,230 @@ function renderChangelogContent(
235
250
  // pi_changelog
236
251
  // ---------------------------------------------------------------------------
237
252
 
238
- export function setupChangelogTool(pi: ExtensionAPI) {
239
- pi.registerTool<typeof ChangelogParamsSchema, ChangelogDetails>({
240
- name: "pi_changelog",
241
- label: "Pi Changelog",
242
- description:
243
- "Get changelog entry for a Pi version. Returns latest by default. Use pi_changelog_versions to list all available versions.",
244
- promptSnippet: `pi_changelog version="1.2.3" // Get changelog for specific version
253
+ const changelogTool = defineTool({
254
+ name: "pi_changelog",
255
+ label: "Pi Changelog",
256
+ description:
257
+ "Get changelog entry for a Pi version. Returns latest by default. Use pi_changelog_versions to list all available versions.",
258
+ promptSnippet: `pi_changelog version="1.2.3" // Get changelog for specific version
245
259
  pi_changelog // Get latest changelog`,
246
- promptGuidelines: [
247
- "Use pi_changelog to check what's new in a Pi version",
248
- "Use pi_changelog_versions first to list available versions",
249
- "Leave version empty for pi_changelog to get the latest changelog",
250
- ],
251
-
252
- parameters: ChangelogParamsSchema,
253
-
254
- async execute(
255
- _toolCallId: string,
256
- params: ChangelogParams,
257
- _signal: AbortSignal | undefined,
258
- _onUpdate: unknown,
259
- _ctx: ExtensionContext,
260
- ): Promise<AgentToolResult<ChangelogDetails>> {
261
- // Newer than installed -> fetch from GitHub
262
- if (params.version && isNewerThanInstalled(params.version)) {
263
- const githubContent = await fetchGithubChangelog();
264
- const changelog = findChangelogEntry(githubContent, params.version);
265
-
266
- const message = `Changelog for ${changelog.version} (from GitHub)\n\n## ${changelog.version}\n\n${changelog.content}`;
267
- return {
268
- content: [{ type: "text", text: message }],
269
- details: {
270
- changelog,
271
- source: "github",
272
- },
273
- };
274
- }
275
-
276
- // Local
277
- const local = readLocalChangelog();
278
- const changelog = findChangelogEntry(local.content, params.version);
279
-
280
- const message = `Changelog for ${changelog.version}\n\n## ${changelog.version}\n\n${changelog.content}`;
260
+ promptGuidelines: [
261
+ "Use pi_changelog to check what's new in a Pi version",
262
+ "Use pi_changelog_versions first to list available versions",
263
+ "Leave version empty for pi_changelog to get the latest changelog",
264
+ ],
265
+
266
+ parameters: ChangelogParamsSchema,
267
+
268
+ async execute(
269
+ _toolCallId,
270
+ params,
271
+ _signal,
272
+ _onUpdate,
273
+ _ctx,
274
+ ): Promise<AgentToolResult<ChangelogDetails>> {
275
+ // Newer than installed -> fetch from GitHub
276
+ if (params.version && isNewerThanInstalled(params.version)) {
277
+ const githubContent = await fetchGithubChangelog();
278
+ const changelog = findChangelogEntry(githubContent, params.version);
279
+
280
+ const message = `Changelog for ${changelog.version} (from GitHub)\n\n## ${changelog.version}\n\n${changelog.content}`;
281
281
  return {
282
282
  content: [{ type: "text", text: message }],
283
283
  details: {
284
284
  changelog,
285
- source: "local",
285
+ source: "github",
286
286
  },
287
287
  };
288
- },
288
+ }
289
289
 
290
- renderCall(args: ChangelogParams, theme: Theme) {
291
- return new ToolCallHeader(
292
- {
293
- toolName: "Pi Changelog",
294
- mainArg: args.version ? `v${args.version}` : "latest",
295
- },
296
- theme,
290
+ // Local
291
+ const local = readLocalChangelog();
292
+ const changelog = findChangelogEntry(local.content, params.version);
293
+
294
+ const message = `Changelog for ${changelog.version}\n\n## ${changelog.version}\n\n${changelog.content}`;
295
+ return {
296
+ content: [{ type: "text", text: message }],
297
+ details: {
298
+ changelog,
299
+ source: "local",
300
+ },
301
+ };
302
+ },
303
+
304
+ renderCall(args, theme) {
305
+ return renderChangelogCall(args, theme);
306
+ },
307
+
308
+ renderResult(result, options, theme) {
309
+ const { details } = result;
310
+
311
+ // Check for missing expected fields to detect errors
312
+ if (!details?.changelog) {
313
+ const text = result.content[0];
314
+ return new Text(
315
+ text?.type === "text" && text.text ? text.text : "No result",
316
+ 0,
317
+ 0,
297
318
  );
298
- },
319
+ }
299
320
 
300
- renderResult(
301
- result: AgentToolResult<ChangelogDetails>,
302
- options: ToolRenderResultOptions,
303
- theme: Theme,
304
- ) {
305
- const { details } = result;
306
-
307
- // Check for missing expected fields to detect errors
308
- if (!details?.changelog) {
309
- const text = result.content[0];
310
- return new Text(
311
- text?.type === "text" && text.text ? text.text : "No result",
312
- 0,
313
- 0,
314
- );
315
- }
321
+ const fields: Array<
322
+ { label: string; value: string; showCollapsed?: boolean } | Text
323
+ > = [];
316
324
 
317
- const fields: Array<
318
- { label: string; value: string; showCollapsed?: boolean } | Text
319
- > = [];
320
-
321
- const lines: string[] = [];
322
-
323
- if (options.expanded) {
324
- // Expanded view: show full changelog content
325
- lines.push(
326
- theme.fg(
327
- "accent",
328
- theme.bold(`Version: ${details.changelog.version}`),
329
- ),
330
- "",
331
- );
332
- lines.push(...renderChangelogContent(details.changelog.content, theme));
333
- fields.push(new Text(lines.join("\n"), 0, 0));
334
- } else {
335
- // Collapsed view: show version + first few lines of changelog + expand hint
336
- lines.push(
337
- theme.fg(
338
- "accent",
339
- theme.bold(`Version: ${details.changelog.version}`),
340
- ),
341
- "",
342
- );
343
- lines.push(
344
- ...renderChangelogContent(
345
- details.changelog.content,
346
- theme,
347
- COLLAPSED_LINES,
348
- ),
349
- );
350
- lines.push(
351
- "",
352
- theme.fg("muted", `${keyHint("app.tools.expand", "to expand")}`),
353
- );
354
- fields.push(new Text(lines.join("\n"), 0, 0));
355
- }
325
+ const lines: string[] = [];
356
326
 
357
- // Footer: show source tag only
358
- const footer = new ToolFooter(theme, {
359
- items: [
360
- {
361
- label: "source",
362
- value: details.source ?? "local",
363
- tone: "accent",
364
- },
365
- ],
366
- });
367
-
368
- return new ToolBody(
369
- {
370
- fields,
371
- footer,
372
- },
373
- options,
374
- theme,
327
+ if (options.expanded) {
328
+ // Expanded view: show full changelog content
329
+ lines.push(
330
+ theme.fg("accent", theme.bold(`Version: ${details.changelog.version}`)),
331
+ "",
375
332
  );
376
- },
377
- });
378
-
379
- // -------------------------------------------------------------------------
380
- // pi_changelog_versions
381
- // -------------------------------------------------------------------------
382
-
383
- pi.registerTool<
384
- typeof ChangelogVersionsParamsSchema,
385
- ChangelogVersionsDetails
386
- >({
387
- name: "pi_changelog_versions",
388
- label: "Pi Changelog Versions",
389
- description: "List all available Pi changelog versions",
390
- promptSnippet: `pi_changelog_versions // List all available versions`,
391
-
392
- parameters: ChangelogVersionsParamsSchema,
393
-
394
- async execute(
395
- _toolCallId: string,
396
- _params: ChangelogVersionsParams,
397
- _signal: AbortSignal | undefined,
398
- _onUpdate: unknown,
399
- _ctx: ExtensionContext,
400
- ): Promise<AgentToolResult<ChangelogVersionsDetails>> {
401
- const local = readLocalChangelog();
402
- const { entries } = parseChangelogEntries(local.content);
403
-
404
- if (entries.length === 0) {
405
- throw new Error("No version entries found in changelog");
406
- }
407
-
408
- const versions = entries.map((e) => e.version);
409
- const message = `${versions.length} versions available:\n${versions.join(", ")}`;
333
+ lines.push(...renderChangelogContent(details.changelog.content, theme));
334
+ fields.push(new Text(lines.join("\n"), 0, 0));
335
+ } else {
336
+ // Collapsed view: show version + first few lines of changelog + expand hint
337
+ lines.push(
338
+ theme.fg("accent", theme.bold(`Version: ${details.changelog.version}`)),
339
+ "",
340
+ );
341
+ lines.push(
342
+ ...renderChangelogContent(
343
+ details.changelog.content,
344
+ theme,
345
+ COLLAPSED_LINES,
346
+ ),
347
+ );
348
+ lines.push(
349
+ "",
350
+ theme.fg("muted", `${keyHint("app.tools.expand", "to expand")}`),
351
+ );
352
+ fields.push(new Text(lines.join("\n"), 0, 0));
353
+ }
410
354
 
411
- return {
412
- content: [{ type: "text", text: message }],
413
- details: {
414
- versions,
415
- source: "local",
355
+ // Footer: show source tag only
356
+ const footer = new ToolFooter(theme, {
357
+ items: [
358
+ {
359
+ label: "source",
360
+ value: details.source ?? "local",
361
+ tone: "accent",
416
362
  },
417
- };
418
- },
419
-
420
- renderCall(_args: ChangelogVersionsParams, theme: Theme) {
421
- return new ToolCallHeader({ toolName: "Pi Changelog Versions" }, theme);
422
- },
423
-
424
- renderResult(
425
- result: AgentToolResult<ChangelogVersionsDetails>,
426
- options: ToolRenderResultOptions,
427
- theme: Theme,
428
- ) {
429
- const { details } = result;
430
-
431
- // Check for missing expected fields to detect errors
432
- if (!details?.versions) {
433
- const text = result.content[0];
434
- return new Text(
435
- text?.type === "text" && text.text ? text.text : "No result",
436
- 0,
437
- 0,
438
- );
439
- }
363
+ ],
364
+ });
365
+
366
+ return new ToolBody(
367
+ {
368
+ fields,
369
+ footer,
370
+ },
371
+ options,
372
+ theme,
373
+ );
374
+ },
375
+ });
440
376
 
441
- const fields: Array<
442
- { label: string; value: string; showCollapsed?: boolean } | Text
443
- > = [];
377
+ // -------------------------------------------------------------------------
378
+ // pi_changelog_versions
379
+ // -------------------------------------------------------------------------
380
+
381
+ const changelogVersionsTool = defineTool({
382
+ name: "pi_changelog_versions",
383
+ label: "Pi Changelog Versions",
384
+ description: "List all available Pi changelog versions",
385
+ promptSnippet: `pi_changelog_versions // List all available versions`,
386
+
387
+ parameters: ChangelogVersionsParamsSchema,
388
+
389
+ async execute(
390
+ _toolCallId,
391
+ _params,
392
+ _signal,
393
+ _onUpdate,
394
+ _ctx,
395
+ ): Promise<AgentToolResult<ChangelogVersionsDetails>> {
396
+ const local = readLocalChangelog();
397
+ const { entries } = parseChangelogEntries(local.content);
398
+
399
+ if (entries.length === 0) {
400
+ throw new Error("No version entries found in changelog");
401
+ }
444
402
 
445
- const lines: string[] = [
446
- theme.fg("accent", `${details.versions.length} versions available:`),
447
- "",
448
- ];
449
- const cols = 6;
450
- const maxLen = Math.max(
451
- ...details.versions.map((version) => version.length),
403
+ const versions = entries.map((e) => e.version);
404
+ const message = `${versions.length} versions available:\n${versions.join(", ")}`;
405
+
406
+ return {
407
+ content: [{ type: "text", text: message }],
408
+ details: {
409
+ versions,
410
+ source: "local",
411
+ },
412
+ };
413
+ },
414
+
415
+ renderCall(args, theme) {
416
+ return renderChangelogVersionsCall(args, theme);
417
+ },
418
+
419
+ renderResult(result, options, theme) {
420
+ const { details } = result;
421
+
422
+ // Check for missing expected fields to detect errors
423
+ if (!details?.versions) {
424
+ const text = result.content[0];
425
+ return new Text(
426
+ text?.type === "text" && text.text ? text.text : "No result",
427
+ 0,
428
+ 0,
452
429
  );
453
- const colWidth = maxLen + 2;
454
- for (let i = 0; i < details.versions.length; i += cols) {
455
- const row = details.versions
456
- .slice(i, i + cols)
457
- .map((version) => version.padEnd(colWidth))
458
- .join("");
459
- lines.push(theme.fg("dim", row));
460
- }
461
- fields.push(new Text(lines.join("\n"), 0, 0));
430
+ }
431
+
432
+ const fields: Array<
433
+ { label: string; value: string; showCollapsed?: boolean } | Text
434
+ > = [];
435
+
436
+ const lines: string[] = [
437
+ theme.fg("accent", `${details.versions.length} versions available:`),
438
+ "",
439
+ ];
440
+ const cols = 6;
441
+ const maxLen = Math.max(
442
+ ...details.versions.map((version) => version.length),
443
+ );
444
+ const colWidth = maxLen + 2;
445
+ for (let i = 0; i < details.versions.length; i += cols) {
446
+ const row = details.versions
447
+ .slice(i, i + cols)
448
+ .map((version) => version.padEnd(colWidth))
449
+ .join("");
450
+ lines.push(theme.fg("dim", row));
451
+ }
452
+ fields.push(new Text(lines.join("\n"), 0, 0));
462
453
 
463
- // Footer: just show version count
464
- const footer = new ToolFooter(theme, {
465
- items: [
466
- {
467
- label: "count",
468
- value: String(details.versions.length),
469
- tone: "accent",
470
- },
471
- ],
472
- });
473
-
474
- return new ToolBody(
454
+ // Footer: just show version count
455
+ const footer = new ToolFooter(theme, {
456
+ items: [
475
457
  {
476
- fields,
477
- footer,
458
+ label: "count",
459
+ value: String(details.versions.length),
460
+ tone: "accent",
478
461
  },
479
- options,
480
- theme,
481
- );
482
- },
483
- });
462
+ ],
463
+ });
464
+
465
+ return new ToolBody(
466
+ {
467
+ fields,
468
+ footer,
469
+ },
470
+ options,
471
+ theme,
472
+ );
473
+ },
474
+ });
475
+
476
+ export function setupChangelogTool(pi: ExtensionAPI) {
477
+ pi.registerTool(changelogTool);
478
+ pi.registerTool(changelogVersionsTool);
484
479
  }