@aliou/pi-dev-kit 0.5.0 → 0.6.1

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.
@@ -13,19 +13,15 @@ import { Text } from "@mariozechner/pi-tui";
13
13
  import { Type } from "@sinclair/typebox";
14
14
  import { findPiInstallation } from "./utils";
15
15
 
16
- const DocsParams = Type.Object({});
17
- type DocsParamsType = Record<string, never>;
16
+ const DocsParamsSchema = Type.Object({});
17
+ type DocsParams = Record<string, never>;
18
18
 
19
19
  interface DocsDetails {
20
- success: boolean;
21
- message: string;
22
20
  /** Relative paths from the pi install root, markdown only. */
23
21
  docFiles?: string[];
24
22
  installPath?: string;
25
23
  }
26
24
 
27
- type ExecuteResult = AgentToolResult<DocsDetails>;
28
-
29
25
  function listFilesRecursive(dir: string, prefix = ""): string[] {
30
26
  const results: string[] = [];
31
27
  if (!fs.existsSync(dir)) return results;
@@ -43,108 +39,78 @@ function listFilesRecursive(dir: string, prefix = ""): string[] {
43
39
  }
44
40
 
45
41
  export function setupDocsTool(pi: ExtensionAPI) {
46
- pi.registerTool<typeof DocsParams, DocsDetails>({
42
+ pi.registerTool<typeof DocsParamsSchema, DocsDetails>({
47
43
  name: "pi_docs",
48
44
  label: "Pi Documentation",
49
45
  description:
50
46
  "List Pi markdown documentation files (README, docs/, examples/)",
51
47
 
52
- parameters: DocsParams,
48
+ promptSnippet: "List Pi documentation files",
49
+ promptGuidelines: [
50
+ "Use to discover available Pi documentation",
51
+ "Returns markdown files from README.md, docs/, and examples/",
52
+ ],
53
+
54
+ parameters: DocsParamsSchema,
53
55
 
54
56
  async execute(
55
57
  _toolCallId: string,
56
- _params: DocsParamsType,
58
+ _params: DocsParams,
57
59
  _signal: AbortSignal | undefined,
58
60
  _onUpdate: unknown,
59
61
  _ctx: ExtensionContext,
60
- ): Promise<ExecuteResult> {
61
- try {
62
- const piPath = findPiInstallation();
63
- if (!piPath) {
64
- return {
65
- content: [
66
- {
67
- type: "text",
68
- text: "Could not locate running Pi installation directory",
69
- },
70
- ],
71
- details: {
72
- success: false,
73
- message: "Could not locate running Pi installation directory",
74
- },
75
- };
76
- }
62
+ ): Promise<AgentToolResult<DocsDetails>> {
63
+ const piPath = findPiInstallation();
64
+ if (!piPath) {
65
+ throw new Error("Could not locate running Pi installation directory");
66
+ }
77
67
 
78
- const readmePath = path.join(piPath, "README.md");
79
- const docsDir = path.join(piPath, "docs");
80
- const examplesDir = path.join(piPath, "examples");
68
+ const readmePath = path.join(piPath, "README.md");
69
+ const docsDir = path.join(piPath, "docs");
70
+ const examplesDir = path.join(piPath, "examples");
81
71
 
82
- const docFiles: string[] = [];
72
+ const docFiles: string[] = [];
83
73
 
84
- if (fs.existsSync(readmePath)) {
85
- docFiles.push("README.md");
86
- }
74
+ if (fs.existsSync(readmePath)) {
75
+ docFiles.push("README.md");
76
+ }
87
77
 
88
- if (fs.existsSync(docsDir)) {
89
- for (const file of listFilesRecursive(docsDir)) {
90
- if (file.endsWith(".md")) {
91
- docFiles.push(`docs/${file}`);
92
- }
78
+ if (fs.existsSync(docsDir)) {
79
+ for (const file of listFilesRecursive(docsDir)) {
80
+ if (file.endsWith(".md")) {
81
+ docFiles.push(`docs/${file}`);
93
82
  }
94
83
  }
84
+ }
95
85
 
96
- if (fs.existsSync(examplesDir)) {
97
- for (const file of listFilesRecursive(examplesDir)) {
98
- if (file.endsWith(".md")) {
99
- docFiles.push(`examples/${file}`);
100
- }
86
+ if (fs.existsSync(examplesDir)) {
87
+ for (const file of listFilesRecursive(examplesDir)) {
88
+ if (file.endsWith(".md")) {
89
+ docFiles.push(`examples/${file}`);
101
90
  }
102
91
  }
92
+ }
103
93
 
104
- if (docFiles.length === 0) {
105
- return {
106
- content: [
107
- {
108
- type: "text",
109
- text: `No markdown documentation found in Pi installation`,
110
- },
111
- ],
112
- details: {
113
- success: false,
114
- message: `No markdown documentation found in Pi installation`,
115
- installPath: piPath,
116
- },
117
- };
118
- }
119
-
120
- // Content sent to LLM: full relative paths so it can read them.
121
- const lines = docFiles.map(
122
- (rel) => `${path.join(piPath, rel)} (${rel})`,
94
+ if (docFiles.length === 0) {
95
+ throw new Error(
96
+ `No markdown documentation found in Pi installation at ${piPath}`,
123
97
  );
124
- const message = `${docFiles.length} markdown files:\n${lines.join("\n")}`;
125
-
126
- return {
127
- content: [{ type: "text", text: message }],
128
- details: {
129
- success: true,
130
- message: `Found ${docFiles.length} markdown files`,
131
- docFiles,
132
- installPath: piPath,
133
- },
134
- };
135
- } catch (error) {
136
- const message = `Error reading Pi documentation: ${error instanceof Error ? error.message : String(error)}`;
137
- return {
138
- content: [{ type: "text", text: message }],
139
- details: {
140
- success: false,
141
- message,
142
- },
143
- };
144
98
  }
99
+
100
+ // Content sent to LLM: full relative paths so it can read them.
101
+ const lines = docFiles.map((rel) => `${path.join(piPath, rel)} (${rel})`);
102
+ const message = `${docFiles.length} markdown files:\n${lines.join("\n")}`;
103
+
104
+ return {
105
+ content: [{ type: "text", text: message }],
106
+ details: {
107
+ docFiles,
108
+ installPath: piPath,
109
+ },
110
+ };
145
111
  },
146
112
 
147
- renderCall(_args: DocsParamsType, theme: Theme) {
113
+ renderCall(_args: DocsParams, theme: Theme) {
148
114
  return new ToolCallHeader({ toolName: "Pi Docs" }, theme);
149
115
  },
150
116
 
@@ -154,8 +120,15 @@ export function setupDocsTool(pi: ExtensionAPI) {
154
120
  theme: Theme,
155
121
  ) {
156
122
  const { details } = result;
123
+ const { isPartial } = options;
124
+
125
+ // Handle isPartial first (this tool doesn't stream, but keep the pattern)
126
+ if (isPartial) {
127
+ return new Text(theme.fg("dim", "Loading..."), 0, 0);
128
+ }
157
129
 
158
- if (!details) {
130
+ // Check for missing expected fields in details to detect errors
131
+ if (!details || !details.docFiles) {
159
132
  const text = result.content[0];
160
133
  return new Text(
161
134
  text?.type === "text" && text.text ? text.text : "No result",
@@ -164,77 +137,45 @@ export function setupDocsTool(pi: ExtensionAPI) {
164
137
  );
165
138
  }
166
139
 
140
+ const { docFiles } = details;
167
141
  const fields: Array<
168
142
  { label: string; value: string; showCollapsed?: boolean } | Text
169
143
  > = [];
170
144
 
171
- if (!details.success) {
172
- fields.push({
173
- label: "Error",
174
- value: theme.fg("error", details.message),
175
- showCollapsed: true,
176
- });
177
- } else if (!details.docFiles || details.docFiles.length === 0) {
178
- fields.push({
179
- label: "Result",
180
- value: theme.fg("warning", "No docs found"),
181
- showCollapsed: true,
182
- });
183
- } else {
145
+ if (options.expanded) {
146
+ // Expanded view: show full file list
184
147
  const lines: string[] = [];
185
-
186
- if (options.expanded) {
187
- lines.push(
188
- theme.fg("accent", `${details.docFiles.length} markdown files:`),
189
- "",
190
- );
191
- for (const rel of details.docFiles) {
192
- lines.push(theme.fg("dim", ` ${rel}`));
193
- }
194
- } else {
195
- lines.push(
196
- theme.fg("accent", `${details.docFiles.length} markdown files`) +
197
- ` (${keyHint("app.tools.expand", "to expand")})`,
198
- "",
199
- );
200
-
201
- const filenames = details.docFiles.map((file) => path.basename(file));
202
- const maxLen = Math.max(...filenames.map((file) => file.length));
203
- const colWidth = maxLen + 2;
204
- const cols = Math.max(1, Math.floor(80 / colWidth));
205
- for (let i = 0; i < filenames.length; i += cols) {
206
- const row = filenames
207
- .slice(i, i + cols)
208
- .map((file) => file.padEnd(colWidth))
209
- .join("");
210
- lines.push(theme.fg("dim", row));
211
- }
148
+ lines.push(
149
+ theme.fg("accent", `${docFiles.length} markdown files:`),
150
+ "",
151
+ );
152
+ for (const rel of docFiles) {
153
+ lines.push(theme.fg("dim", ` ${rel}`));
212
154
  }
213
-
214
155
  fields.push(new Text(lines.join("\n"), 0, 0));
156
+ } else {
157
+ // Collapsed view: show file count + expand hint
158
+ fields.push({
159
+ label: "Files",
160
+ value:
161
+ theme.fg("accent", `${docFiles.length} markdown files`) +
162
+ ` (${keyHint("app.tools.expand", "to expand")})`,
163
+ showCollapsed: true,
164
+ });
215
165
  }
216
166
 
217
- return new ToolBody(
218
- {
219
- fields,
220
- footer: new ToolFooter(theme, {
221
- items: [
222
- {
223
- label: "status",
224
- value: details.success ? "ok" : "error",
225
- tone: details.success ? "success" : "error",
226
- },
227
- {
228
- label: "docs",
229
- value: String(details.docFiles?.length ?? 0),
230
- tone: "accent",
231
- },
232
- ],
233
- }),
234
- },
235
- options,
236
- theme,
237
- );
167
+ // Only show footer if there are items worth showing
168
+ const footer = new ToolFooter(theme, {
169
+ items: [
170
+ {
171
+ label: "docs",
172
+ value: String(docFiles.length),
173
+ tone: "accent",
174
+ },
175
+ ],
176
+ });
177
+
178
+ return new ToolBody({ fields, footer }, options, theme);
238
179
  },
239
180
  });
240
181
  }
@@ -10,14 +10,11 @@ import type {
10
10
  } from "@mariozechner/pi-coding-agent";
11
11
  import { Text } from "@mariozechner/pi-tui";
12
12
  import { Type } from "@sinclair/typebox";
13
- import { resolveCtx } from "./utils";
14
13
 
15
14
  const Params = Type.Object({});
16
- type ParamsType = Record<string, never>;
15
+ type PackageManagerParams = Record<string, never>;
17
16
 
18
17
  interface PackageManagerDetails {
19
- success: boolean;
20
- message: string;
21
18
  packageManager?: string;
22
19
  version?: string;
23
20
  lockfile?: string;
@@ -26,8 +23,6 @@ interface PackageManagerDetails {
26
23
  cwd?: string;
27
24
  }
28
25
 
29
- type ExecuteResult = AgentToolResult<PackageManagerDetails>;
30
-
31
26
  const LOCKFILES: Record<string, string> = {
32
27
  "pnpm-lock.yaml": "pnpm",
33
28
  "yarn.lock": "yarn",
@@ -49,137 +44,115 @@ export function setupPackageManagerTool(pi: ExtensionAPI) {
49
44
  label: "Package Manager",
50
45
  description:
51
46
  "Detect the package manager used in the current project by checking lockfiles and package.json",
47
+ promptSnippet: "Detect the package manager for this project",
48
+ promptGuidelines: [
49
+ "Use when you need to know which package manager (npm, yarn, pnpm, bun) the project uses",
50
+ "Helpful before running install commands or scripts",
51
+ ],
52
52
 
53
53
  parameters: Params,
54
54
 
55
55
  async execute(
56
56
  _toolCallId: string,
57
- _params: ParamsType,
58
- signal: AbortSignal | undefined,
59
- onUpdate: unknown,
57
+ _params: PackageManagerParams,
58
+ _signal: AbortSignal | undefined,
59
+ _onUpdate: unknown,
60
60
  ctx: ExtensionContext,
61
- ): Promise<ExecuteResult> {
62
- const resolvedCtx = resolveCtx(signal, onUpdate, ctx);
63
- const cwd = resolvedCtx.cwd;
64
-
65
- try {
66
- const packageJsonPath = path.join(cwd, "package.json");
67
- if (!fs.existsSync(packageJsonPath)) {
68
- return {
69
- content: [
70
- {
71
- type: "text",
72
- text: `No package.json found in ${cwd}`,
73
- },
74
- ],
75
- details: {
76
- success: false,
77
- message: `No package.json found in ${cwd}`,
78
- cwd,
79
- },
80
- };
81
- }
61
+ ): Promise<AgentToolResult<PackageManagerDetails>> {
62
+ const cwd = ctx.cwd;
82
63
 
83
- // Walk up from cwd to repo root, collecting packageManager and lockfile.
84
- // Stop at .git boundary to avoid escaping the repository.
85
- let declaredPm: string | undefined;
86
- let declaredVersion: string | undefined;
87
- let lockfile: string | undefined;
88
- let lockfilePm: string | undefined;
89
-
90
- let searchDir = cwd;
91
- while (true) {
92
- // Check packageManager field in package.json
93
- if (!declaredPm) {
94
- const pkgPath = path.join(searchDir, "package.json");
95
- try {
96
- if (fs.existsSync(pkgPath)) {
97
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
98
- if (typeof pkg.packageManager === "string") {
99
- const match = pkg.packageManager.match(/^([^@]+)@?(.*)?$/);
100
- if (match) {
101
- declaredPm = match[1];
102
- declaredVersion = match[2] || undefined;
103
- }
64
+ const packageJsonPath = path.join(cwd, "package.json");
65
+ if (!fs.existsSync(packageJsonPath)) {
66
+ throw new Error(`No package.json found in ${cwd}`);
67
+ }
68
+
69
+ // Walk up from cwd to repo root, collecting packageManager and lockfile.
70
+ // Stop at .git boundary to avoid escaping the repository.
71
+ let declaredPm: string | undefined;
72
+ let declaredVersion: string | undefined;
73
+ let lockfile: string | undefined;
74
+ let lockfilePm: string | undefined;
75
+
76
+ let searchDir = cwd;
77
+ while (true) {
78
+ // Check packageManager field in package.json
79
+ if (!declaredPm) {
80
+ const pkgPath = path.join(searchDir, "package.json");
81
+ try {
82
+ if (fs.existsSync(pkgPath)) {
83
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
84
+ if (typeof pkg.packageManager === "string") {
85
+ const match = pkg.packageManager.match(/^([^@]+)@?(.*)?$/);
86
+ if (match) {
87
+ declaredPm = match[1];
88
+ declaredVersion = match[2] || undefined;
104
89
  }
105
90
  }
106
- } catch {
107
- // Ignore parse errors
108
91
  }
92
+ } catch {
93
+ // Ignore parse errors
109
94
  }
95
+ }
110
96
 
111
- // Check lockfiles
112
- if (!lockfilePm) {
113
- for (const [filename, pm] of Object.entries(LOCKFILES)) {
114
- if (fs.existsSync(path.join(searchDir, filename))) {
115
- lockfilePm = pm;
116
- lockfile = filename;
117
- break;
118
- }
97
+ // Check lockfiles
98
+ if (!lockfilePm) {
99
+ for (const [filename, pm] of Object.entries(LOCKFILES)) {
100
+ if (fs.existsSync(path.join(searchDir, filename))) {
101
+ lockfilePm = pm;
102
+ lockfile = filename;
103
+ break;
119
104
  }
120
105
  }
106
+ }
121
107
 
122
- // Stop if we found both, hit .git, or hit filesystem root
123
- if (
124
- (declaredPm && lockfilePm) ||
125
- fs.existsSync(path.join(searchDir, ".git"))
126
- ) {
127
- break;
128
- }
129
- const parent = path.dirname(searchDir);
130
- if (parent === searchDir) break;
131
- searchDir = parent;
108
+ // Stop if we found both, hit .git, or hit filesystem root
109
+ if (
110
+ (declaredPm && lockfilePm) ||
111
+ fs.existsSync(path.join(searchDir, ".git"))
112
+ ) {
113
+ break;
132
114
  }
115
+ const parent = path.dirname(searchDir);
116
+ if (parent === searchDir) break;
117
+ searchDir = parent;
118
+ }
133
119
 
134
- const pm = declaredPm || lockfilePm || "npm";
135
- const fallback = { install: `${pm} install`, run: pm };
136
- const commands = COMMANDS[pm] ?? fallback;
120
+ const pm = declaredPm || lockfilePm || "npm";
121
+ const fallback = { install: `${pm} install`, run: pm };
122
+ const commands = COMMANDS[pm] ?? fallback;
137
123
 
138
- const parts: string[] = [];
139
- parts.push(`Package manager: ${pm}`);
140
- if (declaredVersion) {
141
- parts.push(`Declared version: ${declaredVersion}`);
142
- }
143
- if (lockfile) {
144
- parts.push(`Lockfile: ${lockfile}`);
145
- }
146
- if (!lockfile && !declaredPm) {
147
- parts.push(
148
- "No lockfile or packageManager field found, defaulting to npm",
149
- );
150
- }
151
- parts.push(`Install: ${commands.install}`);
152
- parts.push(`Run: ${commands.run}`);
153
-
154
- const message = parts.join("\n");
155
-
156
- return {
157
- content: [{ type: "text", text: message }],
158
- details: {
159
- success: true,
160
- message,
161
- packageManager: pm,
162
- version: declaredVersion,
163
- lockfile,
164
- installCommand: commands.install,
165
- runCommand: commands.run,
166
- cwd,
167
- },
168
- };
169
- } catch (error) {
170
- const message = `Error detecting package manager: ${error instanceof Error ? error.message : String(error)}`;
171
- return {
172
- content: [{ type: "text", text: message }],
173
- details: {
174
- success: false,
175
- message,
176
- cwd,
177
- },
178
- };
124
+ const parts: string[] = [];
125
+ parts.push(`Package manager: ${pm}`);
126
+ if (declaredVersion) {
127
+ parts.push(`Declared version: ${declaredVersion}`);
179
128
  }
129
+ if (lockfile) {
130
+ parts.push(`Lockfile: ${lockfile}`);
131
+ }
132
+ if (!lockfile && !declaredPm) {
133
+ parts.push(
134
+ "No lockfile or packageManager field found, defaulting to npm",
135
+ );
136
+ }
137
+ parts.push(`Install: ${commands.install}`);
138
+ parts.push(`Run: ${commands.run}`);
139
+
140
+ const message = parts.join("\n");
141
+
142
+ return {
143
+ content: [{ type: "text", text: message }],
144
+ details: {
145
+ packageManager: pm,
146
+ version: declaredVersion,
147
+ lockfile,
148
+ installCommand: commands.install,
149
+ runCommand: commands.run,
150
+ cwd,
151
+ },
152
+ };
180
153
  },
181
154
 
182
- renderCall(_args: ParamsType, theme: Theme) {
155
+ renderCall(_args: PackageManagerParams, theme: Theme) {
183
156
  return new ToolCallHeader({ toolName: "Detect Package Manager" }, theme);
184
157
  },
185
158
 
@@ -190,24 +163,22 @@ export function setupPackageManagerTool(pi: ExtensionAPI) {
190
163
  ): Text {
191
164
  const { details } = result;
192
165
 
193
- if (!details) {
166
+ // Check for missing expected fields (framework passes {} on error)
167
+ if (!details?.packageManager) {
168
+ // Extract error message from result.content
194
169
  const text = result.content[0];
195
- return new Text(
196
- text?.type === "text" && text.text ? text.text : "No result",
197
- 0,
198
- 0,
199
- );
200
- }
201
-
202
- if (!details.success) {
203
- return new Text(theme.fg("error", details.message), 0, 0);
170
+ const errorMessage =
171
+ text?.type === "text" && text.text
172
+ ? text.text
173
+ : "Failed to detect package manager";
174
+ return new Text(theme.fg("error", errorMessage), 0, 0);
204
175
  }
205
176
 
206
177
  const lines: string[] = [];
207
178
  lines.push(
208
179
  theme.fg(
209
180
  "success",
210
- `Package manager: ${theme.bold(details.packageManager || "unknown")}`,
181
+ `Package manager: ${theme.bold(details.packageManager)}`,
211
182
  ),
212
183
  );
213
184
  if (details.version) {
@@ -1,6 +1,5 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
4
3
 
5
4
  /**
6
5
  * Find the currently running Pi installation directory by resolving the
@@ -37,26 +36,3 @@ export function findPiInstallation(): string | null {
37
36
  return null;
38
37
  }
39
38
  }
40
-
41
- /**
42
- * Resolve ExtensionContext from execute args, compatible with Pi 0.50.x and
43
- * 0.51.0+.
44
- *
45
- * Pi 0.50.x: execute(id, params, onUpdate, ctx, signal)
46
- * Pi 0.51.0+: execute(id, params, signal, onUpdate, ctx)
47
- *
48
- * When compiled against 0.51.0+, the typed params are (signal, onUpdate, ctx).
49
- * On 0.50.x at runtime, `signal` actually receives the onUpdate callback
50
- * (a function), `onUpdate` receives ctx, and `ctx` receives signal.
51
- */
52
- export function resolveCtx(
53
- signal: AbortSignal | undefined,
54
- onUpdate: unknown,
55
- ctx: ExtensionContext,
56
- ): ExtensionContext {
57
- // AbortSignal is never callable; a function here means we got onUpdate (0.50.x)
58
- if (typeof signal === "function") {
59
- return onUpdate as ExtensionContext;
60
- }
61
- return ctx;
62
- }
@@ -14,18 +14,18 @@ const VersionParams = Type.Object({});
14
14
  type VersionParamsType = Record<string, never>;
15
15
 
16
16
  interface VersionDetails {
17
- success: boolean;
18
- message: string;
19
- version: string;
17
+ version?: string;
20
18
  }
21
19
 
22
- type ExecuteResult = AgentToolResult<VersionDetails>;
23
-
24
20
  export function setupVersionTool(pi: ExtensionAPI) {
25
21
  pi.registerTool<typeof VersionParams, VersionDetails>({
26
22
  name: "pi_version",
27
23
  label: "Pi Version",
28
24
  description: "Get the version of the currently running Pi instance",
25
+ promptSnippet: "Check the current Pi version.",
26
+ promptGuidelines: [
27
+ "Use when the user asks about the Pi version or when a task depends on knowing the installed version.",
28
+ ],
29
29
 
30
30
  parameters: VersionParams,
31
31
 
@@ -35,15 +35,10 @@ export function setupVersionTool(pi: ExtensionAPI) {
35
35
  _signal: AbortSignal | undefined,
36
36
  _onUpdate: unknown,
37
37
  _ctx: ExtensionContext,
38
- ): Promise<ExecuteResult> {
39
- const message = `Pi version ${VERSION}`;
38
+ ): Promise<AgentToolResult<VersionDetails>> {
40
39
  return {
41
- content: [{ type: "text", text: message }],
42
- details: {
43
- success: true,
44
- message,
45
- version: VERSION,
46
- },
40
+ content: [{ type: "text", text: `Pi version ${VERSION}` }],
41
+ details: { version: VERSION },
47
42
  };
48
43
  },
49
44
 
@@ -58,13 +53,11 @@ export function setupVersionTool(pi: ExtensionAPI) {
58
53
  ): Text {
59
54
  const { details } = result;
60
55
 
61
- if (!details) {
62
- const text = result.content[0];
63
- return new Text(
64
- text?.type === "text" && text.text ? text.text : "No result",
65
- 0,
66
- 0,
67
- );
56
+ if (!details?.version) {
57
+ const textBlock = result.content.find((c) => c.type === "text");
58
+ const msg =
59
+ (textBlock?.type === "text" && textBlock.text) || "Unknown version";
60
+ return new Text(theme.fg("error", msg), 0, 0);
68
61
  }
69
62
 
70
63
  return new Text(