flake-update-20260505
1/**
2 * Pi Extension: Conditional Tool Loading
3 *
4 * Reduces per-request token cost by disabling tools not relevant
5 * to the current workspace. Tools are filtered at session start
6 * based on cwd context (git remotes, project markers, path patterns).
7 *
8 * Commands:
9 * /load-tool [name] — Enable a specific tool (no args = list inactive)
10 * /tools-all — Enable all registered tools
11 * /tools-reset — Re-apply conditional filtering
12 *
13 * Override:
14 * PI_ALL_TOOLS=1 — Disable filtering entirely
15 */
16
17import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18import { existsSync } from "fs";
19import { join } from "path";
20
21// Tools that are always active regardless of context
22const ALWAYS_ACTIVE = new Set([
23 // built-ins
24 "read", "bash", "edit", "write",
25 // personal workflow
26 "org_todo", "get_current_time", "web_search",
27 // session management
28 "find_threads", "search_thread",
29 "save_session_to_history", "list_saved_sessions", "read_saved_session",
30 "save_learning", "save_research", "save_plan",
31 // delegation & worktrees
32 "subagent", "git_worktree",
33]);
34
35// Conditional tools: name → async (cwd, exec) => should_enable
36type ExecFn = (cmd: string, args: string[], opts?: any) => Promise<{ code: number; stdout: string; stderr: string }>;
37
38const CONDITIONAL: Record<string, (cwd: string, exec: ExecFn) => Promise<boolean>> = {
39 github: async (cwd, exec) => {
40 const result = await exec("git", ["remote", "-v"], { cwd, timeout: 3000 });
41 return result.code === 0 && result.stdout.includes("github.com");
42 },
43
44 github_search: async (cwd, exec) => {
45 const result = await exec("git", ["remote", "-v"], { cwd, timeout: 3000 });
46 return result.code === 0 && result.stdout.includes("github.com");
47 },
48
49 jira: async (cwd) => {
50 return /\b(tektoncd|osp|redhat|chapeau-rouge)\b/.test(cwd) ||
51 existsSync(join(cwd, ".jira"));
52 },
53
54 lsp: async (cwd) => {
55 // Match root markers from all LSP server configs in lsp-core.ts
56 return [
57 "flake.nix", "default.nix", "shell.nix", // nix (nil/nixd)
58 "go.mod", "go.work", // go (gopls)
59 "tsconfig.json", "jsconfig.json", "package.json", // typescript
60 "Cargo.toml", // rust
61 "pyproject.toml", "pyrightconfig.json", "setup.py", // python
62 "pubspec.yaml", // dart
63 "settings.gradle", "settings.gradle.kts", // kotlin
64 ].some(f => existsSync(join(cwd, f)));
65 },
66
67 // Pending removal — always disabled
68 kitty_control: async () => false,
69};
70
71export default function (pi: ExtensionAPI) {
72 async function applyFilter(cwd: string): Promise<{ all: string[]; kept: string[]; dropped: string[] }> {
73 const all = pi.getActiveTools();
74 const kept: string[] = [];
75
76 for (const name of all) {
77 if (ALWAYS_ACTIVE.has(name)) {
78 kept.push(name);
79 continue;
80 }
81 const rule = CONDITIONAL[name];
82 if (rule) {
83 try {
84 const enabled = await rule(cwd, pi.exec.bind(pi));
85 if (enabled) kept.push(name);
86 } catch {
87 // On error, keep the tool (safe default)
88 kept.push(name);
89 }
90 continue;
91 }
92 // Unknown tools (from other extensions): keep by default
93 kept.push(name);
94 }
95
96 const dropped = all.filter(n => !kept.includes(n));
97 if (dropped.length > 0) {
98 pi.setActiveTools(kept);
99 }
100
101 return { all, kept, dropped };
102 }
103
104 pi.on("session_start", async (_event, ctx) => {
105 if (process.env.PI_ALL_TOOLS === "1") return;
106 await applyFilter(ctx.cwd);
107 });
108
109 pi.registerCommand("load-tool", {
110 description: "Enable a specific tool for this session (no args = list inactive)",
111 getArgumentCompletions: (prefix) => {
112 const all = pi.getAllTools().map(t => t.name);
113 const active = new Set(pi.getActiveTools());
114 return all
115 .filter(n => !active.has(n) && n.startsWith(prefix))
116 .map(n => ({ value: n, description: "Enable tool" }));
117 },
118 handler: async (args, ctx) => {
119 const name = args.trim();
120 if (!name) {
121 const all = pi.getAllTools().map(t => t.name);
122 const active = new Set(pi.getActiveTools());
123 const inactive = all.filter(n => !active.has(n));
124 ctx.ui.notify(
125 inactive.length > 0
126 ? `Inactive tools: ${inactive.join(", ")}`
127 : "All tools are active",
128 "info",
129 );
130 return;
131 }
132 const allNames = pi.getAllTools().map(t => t.name);
133 if (!allNames.includes(name)) {
134 ctx.ui.notify(`Unknown tool: ${name}`, "error");
135 return;
136 }
137 const active = pi.getActiveTools();
138 if (active.includes(name)) {
139 ctx.ui.notify(`${name} is already active`, "info");
140 return;
141 }
142 pi.setActiveTools([...active, name]);
143 ctx.ui.notify(`Enabled: ${name}`, "info");
144 },
145 });
146
147 pi.registerCommand("tools-all", {
148 description: "Enable all registered tools for this session",
149 handler: async (_args, ctx) => {
150 const all = pi.getAllTools().map(t => t.name);
151 pi.setActiveTools(all);
152 ctx.ui.notify(`All ${all.length} tools enabled`, "info");
153 },
154 });
155
156 pi.registerCommand("tools-reset", {
157 description: "Re-apply conditional tool filtering",
158 handler: async (_args, ctx) => {
159 // Restore all first, then filter
160 const all = pi.getAllTools().map(t => t.name);
161 pi.setActiveTools(all);
162 const { kept, dropped } = await applyFilter(ctx.cwd);
163 const msg = dropped.length > 0
164 ? `Tools: ${kept.length}/${all.length} active (dropped: ${dropped.join(", ")})`
165 : `All ${all.length} tools active (nothing filtered)`;
166 ctx.ui.notify(msg, "info");
167 },
168 });
169}