@dsnbyte/pi-tools-manager 0.0.0-stage → 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/extensions/index.ts +844 -0
- package/package.json +61 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Interactive TUI table to inspect all installed tools, sources (built-in, SDK, npm, local extension), and descriptions.
|
|
9
|
+
- Instant search and filtering by tool name, source, or description.
|
|
10
|
+
- Tool detail view with full parameter documentation, types, and schema inspection.
|
|
11
|
+
- Enable / disable toggle with persistent configuration in `~/.pi/agent/tools-manager.json`.
|
|
12
|
+
- Slash command `/tools [name]` with argument autocomplete.
|
|
13
|
+
- Non-TUI fallback output for print and non-interactive environments.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsnbyte
|
|
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,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-tools-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Pi extension to inspect, search, and toggle installed tools in Pi Coding Agent via an interactive TUI.
|
|
4
|
+
|
|
5
|
+
## Why pi-tools-manager?
|
|
6
|
+
|
|
7
|
+
As you install extensions in Pi, your environment can quickly become crowded with tools:
|
|
8
|
+
|
|
9
|
+
- **Token & Context Overhead**: Every active tool definition and parameter schema is injected into the system prompt, consuming valuable context tokens on every request.
|
|
10
|
+
- **Tool Conflicts & Overlap**: Multiple extensions may register competing tools for the same task (such as multiple web search or browser tools), confusing the model and degrading tool selection accuracy.
|
|
11
|
+
- **Unclear Tool Purpose**: When many tools are loaded, it can be difficult to know what each tool does, which extension provided it, or what parameters it expects.
|
|
12
|
+
|
|
13
|
+
`pi-tools-manager` solves these problems by providing clear visibility and control:
|
|
14
|
+
|
|
15
|
+
- **Inspect Available Tools**: Easily browse all installed tools, see their origin (`builtin` or extension package), and inspect their full descriptions and parameter schemas.
|
|
16
|
+
- **Reduce Token Usage**: Disable tools you do not need for your current project or workflow to keep your prompt lean.
|
|
17
|
+
- **Prevent Conflicts**: Deactivate redundant or overlapping tools so the agent consistently uses the right one.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Interactive TUI List**: View all installed tools as a clean single-line list with their origin (`builtin` or extension name) and descriptions.
|
|
22
|
+
- **Enable / Disable Toggling**: Easily toggle tools on and off. Disabled tools are blocked from agent tool calls and session tools.
|
|
23
|
+
- **Instant Search & Filter**: Type any query to filter tools by name, source, or description.
|
|
24
|
+
- **Detailed Schema Inspection**: Inspect parameter schemas, required flags, default values, and full source paths.
|
|
25
|
+
- **Zero Conversation Clutter**: Operates cleanly in the UI layer without injecting unwanted tokens into the LLM context.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install npm:@dsnbyte/pi-tools-manager
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Try Without Installing
|
|
34
|
+
|
|
35
|
+
You can test the extension in a single Pi session without modifying your settings:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pi -e npm:@dsnbyte/pi-tools-manager
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
You can access the manager anytime during an active Pi session using the `/tools` slash command:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
/tools
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Running `/tools` opens an interactive UI where you can:
|
|
50
|
+
|
|
51
|
+
- **Browse & Search**: View all installed tools and type to instantly filter by tool name, source, or description.
|
|
52
|
+
- **Inspect Details**: Select any tool to inspect its description, parameter schemas, required flags, and source location.
|
|
53
|
+
- **Toggle Tools**: Toggle tools enabled or disabled with `Ctrl+D`. Disabled tools are prevented from being called by the agent, and your configuration persists across sessions.
|
|
54
|
+
|
|
55
|
+
You can also jump directly to the inspection view of a specific tool by providing its name:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
/tools bash
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAgentDir,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
type Theme,
|
|
6
|
+
type ToolInfo,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export interface ToolsManagerOptions {
|
|
13
|
+
configPath?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ToolConfig {
|
|
17
|
+
disabled: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveConfigFile(options?: ToolsManagerOptions): string {
|
|
21
|
+
if (options?.configPath) {
|
|
22
|
+
return options.configPath;
|
|
23
|
+
}
|
|
24
|
+
if (process.env.PI_TOOLS_MANAGER_CONFIG) {
|
|
25
|
+
return process.env.PI_TOOLS_MANAGER_CONFIG;
|
|
26
|
+
}
|
|
27
|
+
return join(getAgentDir(), "tools-manager.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadConfig(configFile: string): ToolConfig {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(configFile)) {
|
|
33
|
+
return { disabled: [] };
|
|
34
|
+
}
|
|
35
|
+
const raw = JSON.parse(readFileSync(configFile, "utf-8")) as unknown;
|
|
36
|
+
const cfg = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
|
|
37
|
+
const disabled = Array.isArray(cfg.disabled)
|
|
38
|
+
? cfg.disabled.filter((n): n is string => typeof n === "string" && n.length > 0)
|
|
39
|
+
: [];
|
|
40
|
+
return { disabled };
|
|
41
|
+
} catch {
|
|
42
|
+
return { disabled: [] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveConfig(configFile: string, cfg: ToolConfig): void {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
49
|
+
writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
50
|
+
} catch {
|
|
51
|
+
// Gracefully ignore write failures (e.g. read-only filesystem)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolved source information for a tool.
|
|
57
|
+
*/
|
|
58
|
+
interface ToolSourceDetail {
|
|
59
|
+
isBuiltin: boolean;
|
|
60
|
+
label: string;
|
|
61
|
+
path?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve where a tool originates from (Built-in Pi or specific Extension).
|
|
66
|
+
*/
|
|
67
|
+
function resolveToolSource(tool: ToolInfo): ToolSourceDetail {
|
|
68
|
+
const sourceInfo = tool.sourceInfo;
|
|
69
|
+
const pathStr = sourceInfo?.path ?? "";
|
|
70
|
+
const sourceStr = sourceInfo?.source ?? "";
|
|
71
|
+
|
|
72
|
+
// 1. Built-in tools in Pi core (e.g. builtin:read, builtin:bash)
|
|
73
|
+
if (sourceStr === "builtin" || pathStr.startsWith("builtin:")) {
|
|
74
|
+
return { isBuiltin: true, label: "builtin", path: pathStr };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Built-in SDK tools (e.g. <sdk:...>)
|
|
78
|
+
if (sourceStr === "sdk" || pathStr.startsWith("<sdk:")) {
|
|
79
|
+
return { isBuiltin: true, label: "builtin", path: pathStr };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. NPM package extensions (e.g. "npm:pi-agent-browser-native", "npm:@upstash/context7-pi")
|
|
83
|
+
if (sourceStr.startsWith("npm:")) {
|
|
84
|
+
const pkgName = sourceStr.replace(/^npm:/, "");
|
|
85
|
+
return {
|
|
86
|
+
isBuiltin: false,
|
|
87
|
+
label: pkgName,
|
|
88
|
+
path: pathStr || sourceInfo?.baseDir,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. File-based extensions in ~/.pi/agent/extensions or <cwd>/.pi/extensions
|
|
93
|
+
const extDirMatch = pathStr.match(/[\\/]extensions[\\/]([^\\/]+)/);
|
|
94
|
+
if (extDirMatch) {
|
|
95
|
+
const rawName = extDirMatch[1];
|
|
96
|
+
const cleanName = rawName.replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
97
|
+
return {
|
|
98
|
+
isBuiltin: false,
|
|
99
|
+
label: cleanName,
|
|
100
|
+
path: pathStr,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Explicit source string if defined and not generic "auto" or "local"
|
|
105
|
+
if (sourceStr && sourceStr !== "auto" && sourceStr !== "local") {
|
|
106
|
+
return {
|
|
107
|
+
isBuiltin: false,
|
|
108
|
+
label: sourceStr,
|
|
109
|
+
path: pathStr,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 6. Base directory fallback
|
|
114
|
+
if (sourceInfo?.baseDir) {
|
|
115
|
+
const parts = sourceInfo.baseDir.split(/[\\/]/).filter(Boolean);
|
|
116
|
+
const dirName = parts[parts.length - 1] || "unknown";
|
|
117
|
+
return {
|
|
118
|
+
isBuiltin: false,
|
|
119
|
+
label: dirName,
|
|
120
|
+
path: pathStr,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 7. File path fallback
|
|
125
|
+
if (pathStr) {
|
|
126
|
+
const parts = pathStr.split(/[\\/]/).filter(Boolean);
|
|
127
|
+
const base = (parts[parts.length - 1] || "unknown").replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
128
|
+
return {
|
|
129
|
+
isBuiltin: false,
|
|
130
|
+
label: base,
|
|
131
|
+
path: pathStr,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
isBuiltin: false,
|
|
137
|
+
label: "extension",
|
|
138
|
+
path: undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Normalize multi-line description into a single line without excessive spaces.
|
|
144
|
+
*/
|
|
145
|
+
function normalizeDesc(desc: string | undefined): string {
|
|
146
|
+
if (!desc) return "(no description)";
|
|
147
|
+
return desc.replace(/\r?\n|\r/g, " ").replace(/\s+/g, " ").trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format a JSON Schema type descriptor into a readable string.
|
|
152
|
+
*/
|
|
153
|
+
function formatType(schema: any): string {
|
|
154
|
+
if (!schema || typeof schema !== "object") return "any";
|
|
155
|
+
if (Array.isArray(schema.enum)) {
|
|
156
|
+
return schema.enum.map((v: unknown) => JSON.stringify(v)).join(" | ");
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(schema.anyOf)) {
|
|
159
|
+
return schema.anyOf.map(formatType).join(" | ");
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(schema.oneOf)) {
|
|
162
|
+
return schema.oneOf.map(formatType).join(" | ");
|
|
163
|
+
}
|
|
164
|
+
if (schema.type === "array") {
|
|
165
|
+
const itemType = schema.items ? formatType(schema.items) : "any";
|
|
166
|
+
return `Array<${itemType}>`;
|
|
167
|
+
}
|
|
168
|
+
if (schema.type) {
|
|
169
|
+
return Array.isArray(schema.type) ? schema.type.join(" | ") : String(schema.type);
|
|
170
|
+
}
|
|
171
|
+
return "any";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Word wrap helper for clean paragraph formatting in detail view.
|
|
176
|
+
*/
|
|
177
|
+
function wrapText(text: string, maxWidth: number, indent = ""): string[] {
|
|
178
|
+
const words = text.split(/\s+/);
|
|
179
|
+
const lines: string[] = [];
|
|
180
|
+
let currentLine = indent;
|
|
181
|
+
|
|
182
|
+
for (const word of words) {
|
|
183
|
+
if (!word) continue;
|
|
184
|
+
if (currentLine.length + word.length + 1 <= maxWidth) {
|
|
185
|
+
currentLine += (currentLine === indent ? "" : " ") + word;
|
|
186
|
+
} else {
|
|
187
|
+
if (currentLine !== indent) lines.push(currentLine);
|
|
188
|
+
currentLine = indent + word;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (currentLine !== indent) lines.push(currentLine);
|
|
192
|
+
return lines.length > 0 ? lines : [indent];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Helper to format shortcut key text separately from its description.
|
|
197
|
+
* Displays the shortcut key in accent color and the description in dim color.
|
|
198
|
+
*/
|
|
199
|
+
function formatKeyHint(th: Theme, key: string, desc: string): string {
|
|
200
|
+
return th.fg("accent", key) + th.fg("dim", ` ${desc}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Format full detail for non-TUI fallback notification.
|
|
205
|
+
*/
|
|
206
|
+
function formatNonTuiDetail(tool: ToolInfo, disabled: Set<string>): string {
|
|
207
|
+
const src = resolveToolSource(tool);
|
|
208
|
+
const status = disabled.has(tool.name) ? "Disabled" : "Enabled";
|
|
209
|
+
const lines: string[] = [
|
|
210
|
+
`Tool: ${tool.name}`,
|
|
211
|
+
`Status: ${status}`,
|
|
212
|
+
`Origin: ${src.label}`,
|
|
213
|
+
`Path: ${src.path || "builtin"}`,
|
|
214
|
+
`Exposure: ${tool.exposure || "direct"}`,
|
|
215
|
+
"",
|
|
216
|
+
"Description:",
|
|
217
|
+
` ${tool.description || "(No description provided)"}`,
|
|
218
|
+
"",
|
|
219
|
+
"Parameters:",
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const params = tool.parameters as any;
|
|
223
|
+
const properties = params?.properties || {};
|
|
224
|
+
const required = new Set(Array.isArray(params?.required) ? params.required : []);
|
|
225
|
+
const propKeys = Object.keys(properties);
|
|
226
|
+
|
|
227
|
+
if (propKeys.length === 0) {
|
|
228
|
+
lines.push(" None (no parameters required)");
|
|
229
|
+
} else {
|
|
230
|
+
for (const key of propKeys) {
|
|
231
|
+
const prop = properties[key] || {};
|
|
232
|
+
const typeStr = formatType(prop);
|
|
233
|
+
const reqStr = required.has(key) ? "required" : "optional";
|
|
234
|
+
const defaultStr = prop.default !== undefined ? ` (default: ${JSON.stringify(prop.default)})` : "";
|
|
235
|
+
lines.push(` • ${key} (${typeStr}, ${reqStr})${defaultStr}`);
|
|
236
|
+
if (prop.description) {
|
|
237
|
+
lines.push(` ${prop.description}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format single-line list for non-TUI fallback notification.
|
|
247
|
+
*/
|
|
248
|
+
function formatNonTuiList(tools: ToolInfo[], disabled: Set<string>): string {
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
for (const t of tools) {
|
|
251
|
+
const src = resolveToolSource(t);
|
|
252
|
+
const dis = disabled.has(t.name) ? " [disabled]" : "";
|
|
253
|
+
const desc = normalizeDesc(t.description);
|
|
254
|
+
lines.push(`${t.name}${dis} [${src.label}] - ${desc}`);
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Interactive TUI Component for displaying installed tools in a single-line list,
|
|
261
|
+
* with keyboard navigation and detailed view upon pressing Enter.
|
|
262
|
+
*/
|
|
263
|
+
class ToolInfoComponent implements Component {
|
|
264
|
+
private tools: ToolInfo[];
|
|
265
|
+
private tui: TUI;
|
|
266
|
+
private theme: Theme;
|
|
267
|
+
private disabled: Set<string>;
|
|
268
|
+
private onToggleTool: (toolName: string) => void;
|
|
269
|
+
private onClose: () => void;
|
|
270
|
+
private mode: "list" | "detail" = "list";
|
|
271
|
+
private selectedIndex = 0;
|
|
272
|
+
private listScrollTop = 0;
|
|
273
|
+
private detailScrollTop = 0;
|
|
274
|
+
private filterText = "";
|
|
275
|
+
private selectedTool: ToolInfo | null = null;
|
|
276
|
+
private cachedWidth?: number;
|
|
277
|
+
private cachedLines?: string[];
|
|
278
|
+
|
|
279
|
+
constructor(
|
|
280
|
+
tools: ToolInfo[],
|
|
281
|
+
tui: TUI,
|
|
282
|
+
theme: Theme,
|
|
283
|
+
disabled: Set<string>,
|
|
284
|
+
onToggleTool: (toolName: string) => void,
|
|
285
|
+
initialTool: ToolInfo | null,
|
|
286
|
+
onClose: () => void,
|
|
287
|
+
) {
|
|
288
|
+
this.tools = tools;
|
|
289
|
+
this.tui = tui;
|
|
290
|
+
this.theme = theme;
|
|
291
|
+
this.disabled = disabled;
|
|
292
|
+
this.onToggleTool = onToggleTool;
|
|
293
|
+
this.onClose = onClose;
|
|
294
|
+
|
|
295
|
+
if (initialTool) {
|
|
296
|
+
this.selectedTool = initialTool;
|
|
297
|
+
this.mode = "detail";
|
|
298
|
+
const idx = tools.findIndex((t) => t.name === initialTool.name);
|
|
299
|
+
if (idx !== -1) this.selectedIndex = idx;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getFilteredTools(): ToolInfo[] {
|
|
304
|
+
if (!this.filterText) return this.tools;
|
|
305
|
+
const kw = this.filterText.toLowerCase();
|
|
306
|
+
return this.tools.filter((t) => {
|
|
307
|
+
const src = resolveToolSource(t);
|
|
308
|
+
return (
|
|
309
|
+
t.name.toLowerCase().includes(kw) ||
|
|
310
|
+
src.label.toLowerCase().includes(kw) ||
|
|
311
|
+
(t.description && t.description.toLowerCase().includes(kw))
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
invalidate(): void {
|
|
317
|
+
this.cachedWidth = undefined;
|
|
318
|
+
this.cachedLines = undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
render(width: number): string[] {
|
|
322
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
323
|
+
return this.cachedLines;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const termRows = this.tui.terminal?.rows || process.stdout.rows || 24;
|
|
327
|
+
let lines: string[];
|
|
328
|
+
|
|
329
|
+
if (this.mode === "detail") {
|
|
330
|
+
lines = this.renderDetail(width, termRows);
|
|
331
|
+
} else {
|
|
332
|
+
lines = this.renderList(width, termRows);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.cachedWidth = width;
|
|
336
|
+
this.cachedLines = lines;
|
|
337
|
+
return lines;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private renderList(width: number, termRows: number): string[] {
|
|
341
|
+
const lines: string[] = [];
|
|
342
|
+
const th = this.theme;
|
|
343
|
+
const filtered = this.getFilteredTools();
|
|
344
|
+
|
|
345
|
+
// Top border in muted tone
|
|
346
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
347
|
+
|
|
348
|
+
// Header bar with tool counts and filter status
|
|
349
|
+
const filterInfo = this.filterText ? ` [Filter: "${this.filterText}"]` : "";
|
|
350
|
+
const disabledCount = this.tools.filter((t) => this.disabled.has(t.name)).length;
|
|
351
|
+
const disabledInfo =
|
|
352
|
+
disabledCount > 0
|
|
353
|
+
? ", " + th.style(`${disabledCount} disabled`, { fg: "error", dim: true, italic: true })
|
|
354
|
+
: "";
|
|
355
|
+
const titleText =
|
|
356
|
+
th.fg("muted", ` Tools (${filtered.length}/${this.tools.length}`) +
|
|
357
|
+
disabledInfo +
|
|
358
|
+
th.fg("muted", `)${filterInfo}`);
|
|
359
|
+
lines.push(titleText);
|
|
360
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
361
|
+
|
|
362
|
+
// Max visible list rows
|
|
363
|
+
const maxVisibleRows = Math.max(6, Math.min(20, termRows - 8));
|
|
364
|
+
|
|
365
|
+
if (filtered.length === 0) {
|
|
366
|
+
lines.push(th.fg("dim", ` (No tools matching "${this.filterText}")`));
|
|
367
|
+
for (let i = 1; i < maxVisibleRows; i++) {
|
|
368
|
+
lines.push("");
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
// Keep selection in range
|
|
372
|
+
this.selectedIndex = Math.max(0, Math.min(filtered.length - 1, this.selectedIndex));
|
|
373
|
+
|
|
374
|
+
// Scroll list if necessary
|
|
375
|
+
if (this.selectedIndex < this.listScrollTop) {
|
|
376
|
+
this.listScrollTop = this.selectedIndex;
|
|
377
|
+
} else if (this.selectedIndex >= this.listScrollTop + maxVisibleRows) {
|
|
378
|
+
this.listScrollTop = this.selectedIndex - maxVisibleRows + 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const start = this.listScrollTop;
|
|
382
|
+
const end = Math.min(filtered.length, start + maxVisibleRows);
|
|
383
|
+
|
|
384
|
+
for (let i = start; i < end; i++) {
|
|
385
|
+
const tool = filtered[i];
|
|
386
|
+
const isSelected = i === this.selectedIndex;
|
|
387
|
+
const isDisabled = this.disabled.has(tool.name);
|
|
388
|
+
const src = resolveToolSource(tool);
|
|
389
|
+
const desc = normalizeDesc(tool.description);
|
|
390
|
+
|
|
391
|
+
const prefix = isSelected ? th.fg("muted", "▸ ") : " ";
|
|
392
|
+
const remainingText = ` [${src.label}] - ${desc}`;
|
|
393
|
+
|
|
394
|
+
if (isSelected) {
|
|
395
|
+
if (isDisabled) {
|
|
396
|
+
const namePart = th.style(tool.name, { fg: "error", italic: true });
|
|
397
|
+
const restPart = th.style(remainingText, { fg: "error", dim: true, italic: true });
|
|
398
|
+
lines.push(truncateToWidth(`${prefix}${namePart}${restPart}`, width - 2));
|
|
399
|
+
} else {
|
|
400
|
+
const namePart = th.bold(th.fg("accent", tool.name));
|
|
401
|
+
const restPart = th.fg("muted", remainingText);
|
|
402
|
+
lines.push(truncateToWidth(`${prefix}${namePart}${restPart}`, width - 2));
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
if (isDisabled) {
|
|
406
|
+
const namePart = th.style(tool.name, { fg: "error", dim: true, italic: true });
|
|
407
|
+
const restPart = th.style(remainingText, { fg: "error", dim: true, italic: true });
|
|
408
|
+
lines.push(truncateToWidth(`${prefix}${namePart}${restPart}`, width - 2));
|
|
409
|
+
} else {
|
|
410
|
+
const namePart = th.fg("accent", tool.name);
|
|
411
|
+
const restPart = th.fg("dim", remainingText);
|
|
412
|
+
lines.push(truncateToWidth(`${prefix}${namePart}${restPart}`, width - 2));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Pad remaining empty rows to maintain layout stability
|
|
418
|
+
for (let i = end - start; i < maxVisibleRows; i++) {
|
|
419
|
+
lines.push("");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Bottom separator & control hint
|
|
424
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
425
|
+
const scrollPos = filtered.length > 0 ? ` (${this.selectedIndex + 1}/${filtered.length})` : "";
|
|
426
|
+
const hints = [
|
|
427
|
+
formatKeyHint(th, "↑/↓", "navigate"),
|
|
428
|
+
formatKeyHint(th, "Enter", "details"),
|
|
429
|
+
formatKeyHint(th, "Ctrl+D", "toggle"),
|
|
430
|
+
formatKeyHint(th, "Type", "to filter"),
|
|
431
|
+
formatKeyHint(th, "Esc", "exit"),
|
|
432
|
+
].join(th.fg("dim", " • "));
|
|
433
|
+
const hintLine = ` ${hints}${th.fg("dim", scrollPos)}`;
|
|
434
|
+
lines.push(truncateToWidth(hintLine, width - 2, ""));
|
|
435
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
436
|
+
|
|
437
|
+
return lines;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private renderDetail(width: number, termRows: number): string[] {
|
|
441
|
+
const tool = this.selectedTool;
|
|
442
|
+
if (!tool) return this.renderList(width, termRows);
|
|
443
|
+
|
|
444
|
+
const th = this.theme;
|
|
445
|
+
const src = resolveToolSource(tool);
|
|
446
|
+
const contentWidth = Math.max(20, width - 6);
|
|
447
|
+
const isDisabled = this.disabled.has(tool.name);
|
|
448
|
+
|
|
449
|
+
const docLines: string[] = [];
|
|
450
|
+
|
|
451
|
+
// Metadata
|
|
452
|
+
const statusText = isDisabled
|
|
453
|
+
? th.style("Disabled", { fg: "error", dim: true, italic: true })
|
|
454
|
+
: th.fg("muted", "Enabled");
|
|
455
|
+
docLines.push(
|
|
456
|
+
th.fg("dim", "Status: ") +
|
|
457
|
+
statusText +
|
|
458
|
+
th.fg("dim", " (") +
|
|
459
|
+
th.fg("accent", "Ctrl+D") +
|
|
460
|
+
th.fg("dim", " to toggle)")
|
|
461
|
+
);
|
|
462
|
+
docLines.push(th.fg("dim", "Origin: ") + th.fg("muted", src.label));
|
|
463
|
+
if (src.path) {
|
|
464
|
+
docLines.push(th.fg("dim", "Source Path: ") + th.fg("dim", src.path));
|
|
465
|
+
}
|
|
466
|
+
docLines.push(th.fg("dim", "Exposure: ") + th.fg("dim", tool.exposure || "direct"));
|
|
467
|
+
docLines.push("");
|
|
468
|
+
|
|
469
|
+
// Description
|
|
470
|
+
docLines.push(th.fg("muted", "Description:"));
|
|
471
|
+
const descText = tool.description || "(No description provided)";
|
|
472
|
+
for (const line of wrapText(descText, contentWidth, " ")) {
|
|
473
|
+
docLines.push(th.fg("dim", line));
|
|
474
|
+
}
|
|
475
|
+
docLines.push("");
|
|
476
|
+
|
|
477
|
+
// Parameters
|
|
478
|
+
docLines.push(th.fg("muted", "Parameters:"));
|
|
479
|
+
const params = tool.parameters as any;
|
|
480
|
+
const properties = params?.properties || {};
|
|
481
|
+
const required = new Set(Array.isArray(params?.required) ? params.required : []);
|
|
482
|
+
const propKeys = Object.keys(properties);
|
|
483
|
+
|
|
484
|
+
if (propKeys.length === 0) {
|
|
485
|
+
docLines.push(th.fg("dim", " None (no parameters required)"));
|
|
486
|
+
} else {
|
|
487
|
+
for (const key of propKeys) {
|
|
488
|
+
const prop = properties[key] || {};
|
|
489
|
+
const typeStr = formatType(prop);
|
|
490
|
+
const reqStr = required.has(key) ? "required" : "optional";
|
|
491
|
+
const defaultStr = prop.default !== undefined ? ` (default: ${JSON.stringify(prop.default)})` : "";
|
|
492
|
+
|
|
493
|
+
docLines.push(
|
|
494
|
+
th.fg("muted", ` • ${key}`) +
|
|
495
|
+
th.fg("dim", ` (${typeStr}, ${reqStr})${defaultStr}`)
|
|
496
|
+
);
|
|
497
|
+
if (prop.description) {
|
|
498
|
+
for (const descLine of wrapText(prop.description, contentWidth, " ")) {
|
|
499
|
+
docLines.push(th.fg("dim", descLine));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
docLines.push("");
|
|
505
|
+
|
|
506
|
+
// Raw Parameters Schema
|
|
507
|
+
docLines.push(th.fg("muted", "Parameters Schema (JSON):"));
|
|
508
|
+
const schemaStr = JSON.stringify(tool.parameters || {}, null, 2);
|
|
509
|
+
for (const line of schemaStr.split("\n")) {
|
|
510
|
+
docLines.push(th.fg("dim", ` ${line}`));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Visible scroll window
|
|
514
|
+
const maxVisibleDetail = Math.max(8, Math.min(24, termRows - 8));
|
|
515
|
+
const maxScroll = Math.max(0, docLines.length - maxVisibleDetail);
|
|
516
|
+
this.detailScrollTop = Math.max(0, Math.min(maxScroll, this.detailScrollTop));
|
|
517
|
+
|
|
518
|
+
const visibleContent = docLines.slice(
|
|
519
|
+
this.detailScrollTop,
|
|
520
|
+
this.detailScrollTop + maxVisibleDetail
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const lines: string[] = [];
|
|
524
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
525
|
+
const statusBadge = isDisabled ? " " + th.style("[DISABLED]", { fg: "error", dim: true, italic: true }) : "";
|
|
526
|
+
lines.push(th.fg("muted", ` TOOL DETAILS: ${tool.name}`) + statusBadge);
|
|
527
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
528
|
+
|
|
529
|
+
for (const line of visibleContent) {
|
|
530
|
+
lines.push(truncateToWidth(` ${line}`, width, ""));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Pad empty lines if content is short
|
|
534
|
+
for (let i = visibleContent.length; i < maxVisibleDetail; i++) {
|
|
535
|
+
lines.push("");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
539
|
+
const scrollInfo = maxScroll > 0 ? ` • (${this.detailScrollTop + 1}/${docLines.length})` : "";
|
|
540
|
+
const toggleDesc = isDisabled ? "enable" : "disable";
|
|
541
|
+
const hints = [
|
|
542
|
+
formatKeyHint(th, "↑/↓", "scroll"),
|
|
543
|
+
formatKeyHint(th, "Ctrl+D", toggleDesc),
|
|
544
|
+
formatKeyHint(th, "Esc", "back to list"),
|
|
545
|
+
].join(th.fg("dim", " • "));
|
|
546
|
+
const hintLine = ` ${hints}${th.fg("dim", scrollInfo)}`;
|
|
547
|
+
lines.push(truncateToWidth(hintLine, width - 2, ""));
|
|
548
|
+
lines.push(th.fg("borderMuted", "─".repeat(width)));
|
|
549
|
+
|
|
550
|
+
return lines;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
handleInput(data: string): void {
|
|
554
|
+
const filtered = this.getFilteredTools();
|
|
555
|
+
|
|
556
|
+
// ----------------------------------------------------
|
|
557
|
+
// Detail Mode Keyboard Handling
|
|
558
|
+
// ----------------------------------------------------
|
|
559
|
+
if (this.mode === "detail") {
|
|
560
|
+
// Toggle disabled state directly from detail page (Ctrl+D only, no space)
|
|
561
|
+
if (matchesKey(data, "ctrl+d") || data === "\x04") {
|
|
562
|
+
if (this.selectedTool) {
|
|
563
|
+
this.onToggleTool(this.selectedTool.name);
|
|
564
|
+
this.invalidate();
|
|
565
|
+
this.tui.requestRender();
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Esc: back to list (previous location)
|
|
571
|
+
if (matchesKey(data, "escape")) {
|
|
572
|
+
this.mode = "list";
|
|
573
|
+
this.invalidate();
|
|
574
|
+
this.tui.requestRender();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
579
|
+
this.onClose();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Scrolling
|
|
584
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
585
|
+
this.detailScrollTop = Math.max(0, this.detailScrollTop - 1);
|
|
586
|
+
this.invalidate();
|
|
587
|
+
this.tui.requestRender();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
592
|
+
this.detailScrollTop++;
|
|
593
|
+
this.invalidate();
|
|
594
|
+
this.tui.requestRender();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (matchesKey(data, "pageUp")) {
|
|
599
|
+
this.detailScrollTop = Math.max(0, this.detailScrollTop - 8);
|
|
600
|
+
this.invalidate();
|
|
601
|
+
this.tui.requestRender();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (matchesKey(data, "pageDown")) {
|
|
606
|
+
this.detailScrollTop += 8;
|
|
607
|
+
this.invalidate();
|
|
608
|
+
this.tui.requestRender();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (matchesKey(data, "home")) {
|
|
613
|
+
this.detailScrollTop = 0;
|
|
614
|
+
this.invalidate();
|
|
615
|
+
this.tui.requestRender();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (matchesKey(data, "end")) {
|
|
620
|
+
this.detailScrollTop = 99999;
|
|
621
|
+
this.invalidate();
|
|
622
|
+
this.tui.requestRender();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ----------------------------------------------------
|
|
630
|
+
// List Mode Keyboard Handling
|
|
631
|
+
// ----------------------------------------------------
|
|
632
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
633
|
+
this.onClose();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
|
|
638
|
+
if (filtered.length > 0 && filtered[this.selectedIndex]) {
|
|
639
|
+
this.selectedTool = filtered[this.selectedIndex];
|
|
640
|
+
this.mode = "detail";
|
|
641
|
+
this.detailScrollTop = 0;
|
|
642
|
+
this.invalidate();
|
|
643
|
+
this.tui.requestRender();
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Toggle disabled state shortcut in list mode (Ctrl+D only, no space)
|
|
649
|
+
if (matchesKey(data, "ctrl+d") || data === "\x04") {
|
|
650
|
+
if (filtered.length > 0 && filtered[this.selectedIndex]) {
|
|
651
|
+
const tool = filtered[this.selectedIndex];
|
|
652
|
+
this.onToggleTool(tool.name);
|
|
653
|
+
this.invalidate();
|
|
654
|
+
this.tui.requestRender();
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (matchesKey(data, "up")) {
|
|
660
|
+
if (filtered.length > 0) {
|
|
661
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
662
|
+
this.invalidate();
|
|
663
|
+
this.tui.requestRender();
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (matchesKey(data, "down")) {
|
|
669
|
+
if (filtered.length > 0) {
|
|
670
|
+
this.selectedIndex = Math.min(filtered.length - 1, this.selectedIndex + 1);
|
|
671
|
+
this.invalidate();
|
|
672
|
+
this.tui.requestRender();
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (matchesKey(data, "pageUp")) {
|
|
678
|
+
if (filtered.length > 0) {
|
|
679
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 8);
|
|
680
|
+
this.invalidate();
|
|
681
|
+
this.tui.requestRender();
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (matchesKey(data, "pageDown")) {
|
|
687
|
+
if (filtered.length > 0) {
|
|
688
|
+
this.selectedIndex = Math.min(filtered.length - 1, this.selectedIndex + 8);
|
|
689
|
+
this.invalidate();
|
|
690
|
+
this.tui.requestRender();
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (matchesKey(data, "home")) {
|
|
696
|
+
this.selectedIndex = 0;
|
|
697
|
+
this.invalidate();
|
|
698
|
+
this.tui.requestRender();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (matchesKey(data, "end")) {
|
|
703
|
+
this.selectedIndex = Math.max(0, filtered.length - 1);
|
|
704
|
+
this.invalidate();
|
|
705
|
+
this.tui.requestRender();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
|
|
710
|
+
if (this.filterText.length > 0) {
|
|
711
|
+
this.filterText = this.filterText.slice(0, -1);
|
|
712
|
+
this.selectedIndex = 0;
|
|
713
|
+
this.listScrollTop = 0;
|
|
714
|
+
this.invalidate();
|
|
715
|
+
this.tui.requestRender();
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Printable character: add to search filter
|
|
721
|
+
if (data.length === 1 && data >= " " && data <= "~") {
|
|
722
|
+
if (data === " " && this.filterText.length === 0) return;
|
|
723
|
+
this.filterText += data;
|
|
724
|
+
this.selectedIndex = 0;
|
|
725
|
+
this.listScrollTop = 0;
|
|
726
|
+
this.invalidate();
|
|
727
|
+
this.tui.requestRender();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Tools Manager Extension for Pi.
|
|
735
|
+
* Registers the /tools slash command to display installed tools in a clean,
|
|
736
|
+
* keyboard-navigable table with darker/muted colors, showing details on Enter.
|
|
737
|
+
* Operates purely in the UI layer without entering the conversation context.
|
|
738
|
+
*/
|
|
739
|
+
export default function toolsManagerExtension(pi: ExtensionAPI, options?: ToolsManagerOptions) {
|
|
740
|
+
const configFile = resolveConfigFile(options);
|
|
741
|
+
const disabled = new Set<string>(loadConfig(configFile).disabled);
|
|
742
|
+
|
|
743
|
+
function persist(): void {
|
|
744
|
+
saveConfig(configFile, { disabled: Array.from(disabled).sort() });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function applyTools(): void {
|
|
748
|
+
const active = pi.getActiveTools();
|
|
749
|
+
const next = active.filter((name) => !disabled.has(name));
|
|
750
|
+
if (next.length !== active.length) pi.setActiveTools(next);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function enableTool(name: string): void {
|
|
754
|
+
if (!disabled.delete(name)) return;
|
|
755
|
+
const active = pi.getActiveTools();
|
|
756
|
+
if (!active.includes(name)) pi.setActiveTools([...active, name]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function disableTool(name: string): void {
|
|
760
|
+
disabled.add(name);
|
|
761
|
+
applyTools();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function toggleTool(name: string): boolean {
|
|
765
|
+
if (disabled.has(name)) {
|
|
766
|
+
enableTool(name);
|
|
767
|
+
} else {
|
|
768
|
+
disableTool(name);
|
|
769
|
+
}
|
|
770
|
+
persist();
|
|
771
|
+
return disabled.has(name);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Event hooks matching tool management exactly
|
|
775
|
+
pi.on("session_start", () => {
|
|
776
|
+
applyTools();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
pi.on("before_agent_start", () => {
|
|
780
|
+
applyTools();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
pi.on("tool_call", (event) => {
|
|
784
|
+
if (disabled.has(event.toolName)) {
|
|
785
|
+
return {
|
|
786
|
+
block: true,
|
|
787
|
+
reason: `Tool ${event.toolName} is disabled`,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return undefined;
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
pi.registerCommand("tools", {
|
|
794
|
+
description: "Display installed tools list and details, toggle enabled/disabled status",
|
|
795
|
+
getArgumentCompletions: (prefix) => {
|
|
796
|
+
const tools = pi.getAllTools();
|
|
797
|
+
const names = tools.map((t) => t.name);
|
|
798
|
+
const search = prefix.trim().toLowerCase();
|
|
799
|
+
const filtered = names.filter((name) => name.toLowerCase().startsWith(search));
|
|
800
|
+
return filtered.length > 0 ? filtered.map((c) => ({ value: c, label: c })) : null;
|
|
801
|
+
},
|
|
802
|
+
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
803
|
+
const tools = pi.getAllTools();
|
|
804
|
+
const trimmedArg = args.trim();
|
|
805
|
+
|
|
806
|
+
// Find initial tool if argument was passed (e.g. `/tools bash`)
|
|
807
|
+
let initialTool: ToolInfo | null = null;
|
|
808
|
+
if (trimmedArg) {
|
|
809
|
+
const found = tools.find((t) => t.name.toLowerCase() === trimmedArg.toLowerCase());
|
|
810
|
+
if (!found) {
|
|
811
|
+
ctx.ui.notify(
|
|
812
|
+
`Tool "${trimmedArg}" not found. Run /tools to view all installed tools.`,
|
|
813
|
+
"warning",
|
|
814
|
+
);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
initialTool = found;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Non-TUI mode fallback (print, RPC without TUI)
|
|
821
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
822
|
+
if (initialTool) {
|
|
823
|
+
ctx.ui.notify(formatNonTuiDetail(initialTool, disabled), "info");
|
|
824
|
+
} else {
|
|
825
|
+
ctx.ui.notify(formatNonTuiList(tools, disabled), "info");
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Interactive TUI mode: open custom table component
|
|
831
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
832
|
+
return new ToolInfoComponent(
|
|
833
|
+
tools,
|
|
834
|
+
tui,
|
|
835
|
+
theme,
|
|
836
|
+
disabled,
|
|
837
|
+
(toolName) => toggleTool(toolName),
|
|
838
|
+
initialTool,
|
|
839
|
+
() => done(),
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsnbyte/pi-tools-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension to inspect, search, and toggle installed tools via an interactive TUI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "dsnbyte",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dsnbyte/pi-tools-manager.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/dsnbyte/pi-tools-manager/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/dsnbyte/pi-tools-manager#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"tools",
|
|
23
|
+
"tool-manager",
|
|
24
|
+
"tui"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"extensions",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./extensions/index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-agent-core": "*",
|
|
39
|
+
"@earendil-works/pi-ai": "*",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
41
|
+
"@earendil-works/pi-tui": "*"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@earendil-works/pi-agent-core": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@earendil-works/pi-ai": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@earendil-works/pi-coding-agent": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-tui": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
59
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
60
|
+
"@types/node": "^26.6.6",
|
|
61
|
+
"typescript": "^7.0.2"
|
|
62
|
+
}
|
|
63
|
+
}
|