@bdsqqq/pi 0.0.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.
Files changed (99) hide show
  1. package/dist/chunk-DsIazq42.js +26 -0
  2. package/dist/command-palette-B97yKqne.js +406 -0
  3. package/dist/core/agents-md.d.ts +22 -0
  4. package/dist/core/agents-md.js +174 -0
  5. package/dist/core/box-chrome.d.ts +56 -0
  6. package/dist/core/box-chrome.js +207 -0
  7. package/dist/core/box-format.d.ts +82 -0
  8. package/dist/core/box-format.js +381 -0
  9. package/dist/core/config.d.ts +14 -0
  10. package/dist/core/config.js +201 -0
  11. package/dist/core/file-tracker.d.ts +65 -0
  12. package/dist/core/file-tracker.js +439 -0
  13. package/dist/core/github-api.d.ts +45 -0
  14. package/dist/core/github-api.js +101 -0
  15. package/dist/core/html-to-md.d.ts +5 -0
  16. package/dist/core/html-to-md.js +289 -0
  17. package/dist/core/interpolate.d.ts +44 -0
  18. package/dist/core/interpolate.js +475 -0
  19. package/dist/core/mutex.d.ts +8 -0
  20. package/dist/core/mutex.js +142 -0
  21. package/dist/core/output-buffer.d.ts +80 -0
  22. package/dist/core/output-buffer.js +517 -0
  23. package/dist/core/permissions.d.ts +19 -0
  24. package/dist/core/permissions.js +140 -0
  25. package/dist/core/pi-spawn.d.ts +62 -0
  26. package/dist/core/pi-spawn.js +244 -0
  27. package/dist/core/prompt-patch.d.ts +11 -0
  28. package/dist/core/prompt-patch.js +16 -0
  29. package/dist/core/show-renderer.d.ts +10 -0
  30. package/dist/core/show-renderer.js +39 -0
  31. package/dist/core/show.d.ts +29 -0
  32. package/dist/core/show.js +288 -0
  33. package/dist/core/sub-agent-render.d.ts +56 -0
  34. package/dist/core/sub-agent-render.js +579 -0
  35. package/dist/core/tool-cost.d.ts +17 -0
  36. package/dist/core/tool-cost.js +58 -0
  37. package/dist/core/tui.d.ts +9 -0
  38. package/dist/core/tui.js +33 -0
  39. package/dist/editor-BljciPdx.js +537 -0
  40. package/dist/extensions/bash.d.ts +12 -0
  41. package/dist/extensions/bash.js +261 -0
  42. package/dist/extensions/code-review.d.ts +14 -0
  43. package/dist/extensions/code-review.js +175 -0
  44. package/dist/extensions/command-palette.d.ts +6 -0
  45. package/dist/extensions/command-palette.js +2 -0
  46. package/dist/extensions/create-file.d.ts +7 -0
  47. package/dist/extensions/create-file.js +88 -0
  48. package/dist/extensions/edit-file.d.ts +7 -0
  49. package/dist/extensions/edit-file.js +395 -0
  50. package/dist/extensions/editor.d.ts +6 -0
  51. package/dist/extensions/editor.js +2 -0
  52. package/dist/extensions/finder.d.ts +13 -0
  53. package/dist/extensions/finder.js +108 -0
  54. package/dist/extensions/format-file.d.ts +11 -0
  55. package/dist/extensions/format-file.js +145 -0
  56. package/dist/extensions/github.d.ts +13 -0
  57. package/dist/extensions/github.js +527 -0
  58. package/dist/extensions/glob.d.ts +10 -0
  59. package/dist/extensions/glob.js +163 -0
  60. package/dist/extensions/grep.d.ts +13 -0
  61. package/dist/extensions/grep.js +356 -0
  62. package/dist/extensions/handoff.d.ts +6 -0
  63. package/dist/extensions/handoff.js +277 -0
  64. package/dist/extensions/librarian.d.ts +13 -0
  65. package/dist/extensions/librarian.js +113 -0
  66. package/dist/extensions/look-at.d.ts +13 -0
  67. package/dist/extensions/look-at.js +121 -0
  68. package/dist/extensions/ls.d.ts +8 -0
  69. package/dist/extensions/ls.js +87 -0
  70. package/dist/extensions/mermaid.d.ts +20 -0
  71. package/dist/extensions/mermaid.js +2 -0
  72. package/dist/extensions/oracle.d.ts +13 -0
  73. package/dist/extensions/oracle.js +143 -0
  74. package/dist/extensions/read-session.d.ts +13 -0
  75. package/dist/extensions/read-session.js +265 -0
  76. package/dist/extensions/read-web-page.d.ts +11 -0
  77. package/dist/extensions/read-web-page.js +234 -0
  78. package/dist/extensions/read.d.ts +23 -0
  79. package/dist/extensions/read.js +323 -0
  80. package/dist/extensions/search-sessions.d.ts +29 -0
  81. package/dist/extensions/search-sessions.js +426 -0
  82. package/dist/extensions/session-name.d.ts +6 -0
  83. package/dist/extensions/session-name.js +54 -0
  84. package/dist/extensions/skill.d.ts +7 -0
  85. package/dist/extensions/skill.js +232 -0
  86. package/dist/extensions/system-prompt.d.ts +6 -0
  87. package/dist/extensions/system-prompt.js +31 -0
  88. package/dist/extensions/task.d.ts +11 -0
  89. package/dist/extensions/task.js +114 -0
  90. package/dist/extensions/tool-harness.d.ts +6 -0
  91. package/dist/extensions/tool-harness.js +37 -0
  92. package/dist/extensions/undo-edit.d.ts +7 -0
  93. package/dist/extensions/undo-edit.js +127 -0
  94. package/dist/extensions/web-search.d.ts +12 -0
  95. package/dist/extensions/web-search.js +254 -0
  96. package/dist/extensions.d.ts +29 -0
  97. package/dist/extensions.js +29 -0
  98. package/dist/mermaid-C7xCVBrw.js +95296 -0
  99. package/package.json +67 -0
@@ -0,0 +1,232 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed, textSection } from "../core/box-format.js";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ import { Type } from "@sinclair/typebox";
7
+ import * as os from "node:os";
8
+ //#region packages/extensions/skill/index.ts
9
+ /**
10
+ * skill tool — load a skill by name, returning its content for
11
+ * injection into the conversation context.
12
+ *
13
+ * replaces pi's default approach (model uses `read` on the SKILL.md
14
+ * path) with a dedicated tool. the model
15
+ * calls `skill(name: "git")` instead of `read(path: "/.../SKILL.md")`.
16
+ *
17
+ * discovery searches skill directories configured in pi's settings
18
+ * (settings.json `skills` array), the default agentDir/skills/,
19
+ * and project-local .pi/skills/. frontmatter is parsed for name
20
+ * and description. files in the skill directory are listed in
21
+ * <skill_files> for the model to read if needed.
22
+ *
23
+ * does NOT inject MCP or builtin tools from frontmatter — that
24
+ * requires runtime tool registration which is out of scope for
25
+ * this first pass.
26
+ */
27
+ const COLLAPSED_EXCERPTS = [{
28
+ focus: "head",
29
+ context: 3
30
+ }, {
31
+ focus: "tail",
32
+ context: 5
33
+ }];
34
+ function parseFrontmatter(content) {
35
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
36
+ if (!normalized.startsWith("---")) return {
37
+ frontmatter: {},
38
+ body: normalized
39
+ };
40
+ const endIndex = normalized.indexOf("\n---", 3);
41
+ if (endIndex === -1) return {
42
+ frontmatter: {},
43
+ body: normalized
44
+ };
45
+ const yamlStr = normalized.slice(4, endIndex);
46
+ const body = normalized.slice(endIndex + 4).trim();
47
+ const frontmatter = {};
48
+ for (const line of yamlStr.split("\n")) {
49
+ const match = line.match(/^(\w[\w-]*):\s*"?(.+?)"?\s*$/);
50
+ if (match && match[1] && match[2]) frontmatter[match[1]] = match[2];
51
+ }
52
+ return {
53
+ frontmatter,
54
+ body
55
+ };
56
+ }
57
+ /**
58
+ * resolve agentDir the same way pi does:
59
+ * env PI_CODING_AGENT_DIR > ~/.pi/agent/
60
+ */
61
+ function getAgentDir() {
62
+ const envDir = process.env.PI_CODING_AGENT_DIR;
63
+ if (envDir) {
64
+ if (envDir === "~") return os.homedir();
65
+ if (envDir.startsWith("~/")) return os.homedir() + envDir.slice(1);
66
+ return envDir;
67
+ }
68
+ return path.join(os.homedir(), ".pi", "agent");
69
+ }
70
+ /**
71
+ * read pi's settings.json to get additional skill paths.
72
+ * returns the `skills` array if present.
73
+ */
74
+ function getSkillPathsFromSettings() {
75
+ const settingsPath = path.join(getAgentDir(), "settings.json");
76
+ if (!fs.existsSync(settingsPath)) return [];
77
+ try {
78
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
79
+ if (Array.isArray(settings.skills)) return settings.skills.map((p) => {
80
+ if (p === "~") return os.homedir();
81
+ if (p.startsWith("~/")) return os.homedir() + p.slice(1);
82
+ return p;
83
+ });
84
+ } catch {}
85
+ return [];
86
+ }
87
+ /**
88
+ * search for a skill by name across all known directories.
89
+ * checks: agentDir/skills/{name}/SKILL.md, settings skill paths,
90
+ * and project-local .pi/skills/{name}/SKILL.md.
91
+ */
92
+ function findSkill(name, cwd) {
93
+ const candidates = [];
94
+ candidates.push(path.join(getAgentDir(), "skills", name, "SKILL.md"));
95
+ for (const skillDir of getSkillPathsFromSettings()) candidates.push(path.join(skillDir, name, "SKILL.md"));
96
+ candidates.push(path.join(cwd, ".pi", "skills", name, "SKILL.md"));
97
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return {
98
+ name,
99
+ filePath: candidate,
100
+ baseDir: path.dirname(candidate)
101
+ };
102
+ return null;
103
+ }
104
+ /**
105
+ * list all known skill names for error messages.
106
+ */
107
+ function listAvailableSkills(cwd) {
108
+ const names = /* @__PURE__ */ new Set();
109
+ const dirs = [
110
+ path.join(getAgentDir(), "skills"),
111
+ ...getSkillPathsFromSettings(),
112
+ path.join(cwd, ".pi", "skills")
113
+ ];
114
+ for (const dir of dirs) {
115
+ if (!fs.existsSync(dir)) continue;
116
+ try {
117
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
118
+ if (!entry.isDirectory()) continue;
119
+ const skillMd = path.join(dir, entry.name, "SKILL.md");
120
+ if (fs.existsSync(skillMd)) names.add(entry.name);
121
+ }
122
+ } catch {}
123
+ }
124
+ return Array.from(names).sort();
125
+ }
126
+ /**
127
+ * collect file paths in the skill directory (excluding SKILL.md),
128
+ * walking subdirectories. used for the <skill_files> block so the
129
+ * model knows what reference files are available.
130
+ */
131
+ function collectSkillFiles(baseDir) {
132
+ const files = [];
133
+ function walk(dir) {
134
+ let entries;
135
+ try {
136
+ entries = fs.readdirSync(dir, { withFileTypes: true });
137
+ } catch {
138
+ return;
139
+ }
140
+ for (const entry of entries) {
141
+ if (entry.name.startsWith(".")) continue;
142
+ if (entry.name === "node_modules") continue;
143
+ const full = path.join(dir, entry.name);
144
+ if (entry.isDirectory()) walk(full);
145
+ else if (entry.isFile() && entry.name !== "SKILL.md") files.push(full);
146
+ }
147
+ }
148
+ walk(baseDir);
149
+ return files;
150
+ }
151
+ function createSkillTool() {
152
+ return {
153
+ name: "skill",
154
+ label: "Load Skill",
155
+ description: "Load a specialized skill that provides domain-specific instructions and workflows.\n\nWhen you recognize that a task matches one of the available skills, use this tool to load the full skill instructions.\n\nThe skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
156
+ parameters: Type.Object({
157
+ name: Type.String({ description: "The name of the skill to load (must match one of the available skills)." }),
158
+ arguments: Type.Optional(Type.String({ description: "Optional arguments to pass to the skill." }))
159
+ }),
160
+ renderCall(args, theme) {
161
+ const name = args.name || "...";
162
+ return new Text(theme.fg("dim", "using ") + theme.fg("toolTitle", theme.bold(name)) + theme.fg("dim", " skill"), 0, 0);
163
+ },
164
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
165
+ const p = params;
166
+ const skill = findSkill(p.name, ctx.cwd);
167
+ if (!skill) {
168
+ const available = listAvailableSkills(ctx.cwd);
169
+ const list = available.length > 0 ? `\n\navailable skills: ${available.join(", ")}` : "";
170
+ return {
171
+ content: [{
172
+ type: "text",
173
+ text: `skill "${p.name}" not found.${list}`
174
+ }],
175
+ isError: true
176
+ };
177
+ }
178
+ let rawContent;
179
+ try {
180
+ rawContent = fs.readFileSync(skill.filePath, "utf-8");
181
+ } catch (err) {
182
+ return {
183
+ content: [{
184
+ type: "text",
185
+ text: `failed to read skill file: ${err.message}`
186
+ }],
187
+ isError: true
188
+ };
189
+ }
190
+ const { body } = parseFrontmatter(rawContent);
191
+ const parts = [
192
+ `<loaded_skill name="${skill.name}">`,
193
+ body,
194
+ "",
195
+ `Base directory for this skill: file://${skill.baseDir}`,
196
+ "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory."
197
+ ];
198
+ const skillFiles = collectSkillFiles(skill.baseDir);
199
+ if (skillFiles.length > 0) {
200
+ parts.push("");
201
+ parts.push("<skill_files>");
202
+ for (const f of skillFiles) parts.push(`<file>${f}</file>`);
203
+ parts.push("</skill_files>");
204
+ }
205
+ parts.push("</loaded_skill>");
206
+ return {
207
+ content: [{
208
+ type: "text",
209
+ text: parts.join("\n")
210
+ }],
211
+ details: { header: skill.name }
212
+ };
213
+ },
214
+ renderResult(result, { expanded }, _theme) {
215
+ const content = result.content?.[0];
216
+ if (!content || content.type !== "text") return new Text("(no output)", 0, 0);
217
+ if (content.text.startsWith("<loaded_skill")) return boxRendererWindowed(() => [textSection(void 0, "skill loaded", true)], {
218
+ collapsed: {},
219
+ expanded: {}
220
+ }, void 0, expanded);
221
+ return boxRendererWindowed(() => [textSection(void 0, content.text)], {
222
+ collapsed: { excerpts: COLLAPSED_EXCERPTS },
223
+ expanded: {}
224
+ }, void 0, expanded);
225
+ }
226
+ };
227
+ }
228
+ function skill_default(pi) {
229
+ pi.registerTool(withPromptPatch(createSkillTool()));
230
+ }
231
+ //#endregion
232
+ export { createSkillTool, skill_default as default };
@@ -0,0 +1,6 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/system-prompt/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): void;
5
+ //#endregion
6
+ export { export_default as default };
@@ -0,0 +1,31 @@
1
+ import { getExtensionConfig } from "../core/config.js";
2
+ import { interpolatePromptVars } from "../core/interpolate.js";
3
+ import { resolvePrompt } from "../core/pi-spawn.js";
4
+ //#region packages/extensions/system-prompt/index.ts
5
+ const CONFIG_DEFAULTS = {
6
+ identity: "Amp",
7
+ harness: "pi",
8
+ promptFile: "",
9
+ promptString: "",
10
+ harnessDocsPromptFile: "",
11
+ harnessDocsPromptString: ""
12
+ };
13
+ function system_prompt_default(pi) {
14
+ const cfg = getExtensionConfig("@bds_pi/system-prompt", CONFIG_DEFAULTS);
15
+ const body = resolvePrompt(cfg.promptString, cfg.promptFile);
16
+ if (!body) return;
17
+ const harnessDocsFile = cfg.harnessDocsPromptFile || `prompt.harness-docs.${cfg.harness}.md`;
18
+ const harnessDocs = resolvePrompt(cfg.harnessDocsPromptString, harnessDocsFile);
19
+ pi.on("before_agent_start", async (event, ctx) => {
20
+ const interpolated = interpolatePromptVars(body, ctx.cwd, {
21
+ sessionId: ctx.sessionManager.getSessionId(),
22
+ identity: cfg.identity,
23
+ harness: cfg.harness,
24
+ harnessDocsSection: harnessDocs
25
+ });
26
+ if (!interpolated.trim()) return;
27
+ return { systemPrompt: event.systemPrompt + "\n\n" + interpolated };
28
+ });
29
+ }
30
+ //#endregion
31
+ export { system_prompt_default as default };
@@ -0,0 +1,11 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/task/index.d.ts
4
+ interface TaskConfig {
5
+ builtinTools?: string[];
6
+ extensionTools?: string[];
7
+ }
8
+ declare function createTaskTool(config?: TaskConfig): ToolDefinition;
9
+ declare function export_default(pi: ExtensionAPI): void;
10
+ //#endregion
11
+ export { TaskConfig, createTaskTool, export_default as default };
@@ -0,0 +1,114 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { getExtensionConfig } from "../core/config.js";
3
+ import { piSpawn, zeroUsage } from "../core/pi-spawn.js";
4
+ import { getFinalOutput, renderAgentTree, subAgentResult } from "../core/sub-agent-render.js";
5
+ import { Container, Text } from "@mariozechner/pi-tui";
6
+ import { Type } from "@sinclair/typebox";
7
+ //#region packages/extensions/task/index.ts
8
+ const CONFIG_DEFAULTS = {
9
+ builtinTools: [
10
+ "read",
11
+ "grep",
12
+ "find",
13
+ "ls",
14
+ "bash",
15
+ "edit",
16
+ "write"
17
+ ],
18
+ extensionTools: [
19
+ "read",
20
+ "grep",
21
+ "find",
22
+ "ls",
23
+ "bash",
24
+ "edit",
25
+ "write",
26
+ "format_file",
27
+ "skill",
28
+ "finder"
29
+ ]
30
+ };
31
+ function createTaskTool(config = {}) {
32
+ return {
33
+ name: "Task",
34
+ label: "Task",
35
+ description: "Perform a task (a sub-task of the user's overall task) using a sub-agent that has access to the following tools: Read, Grep, Find, ls, Bash, Edit, Write, format_file, skill, finder.\n\nWhen to use the Task tool:\n- When you need to perform complex multi-step tasks\n- When you need to run an operation that will produce a lot of output (tokens) that is not needed after the sub-agent's task completes\n- When you are making changes across many layers of an application, after you have first planned and spec'd out the changes so they can be implemented independently\n- When the user asks you to launch an \"agent\" or \"subagent\"\n\nWhen NOT to use the Task tool:\n- When you are performing a single logical task\n- When you're reading a single file (use Read), performing a text search (use Grep), editing a single file (use Edit)\n- When you're not sure what changes you want to make\n\nHow to use the Task tool:\n- Run multiple sub-agents concurrently if tasks are independent, by including multiple tool uses in a single assistant message.\n- Include all necessary context and a detailed plan in the task description.\n- Tell the sub-agent how to verify its work if possible.\n- When the agent is done, it will return a single message back to you.",
36
+ parameters: Type.Object({
37
+ prompt: Type.String({ description: "The task for the agent to perform. Be specific and include any relevant context." }),
38
+ description: Type.String({ description: "A very short description of the task that can be displayed to the user." })
39
+ }),
40
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
41
+ const p = params;
42
+ let sessionId = "";
43
+ try {
44
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
45
+ } catch {}
46
+ const singleResult = {
47
+ agent: "Task",
48
+ task: p.description,
49
+ exitCode: -1,
50
+ messages: [],
51
+ usage: zeroUsage()
52
+ };
53
+ const result = await piSpawn({
54
+ cwd: ctx.cwd,
55
+ task: p.prompt,
56
+ builtinTools: config.builtinTools ?? CONFIG_DEFAULTS.builtinTools,
57
+ extensionTools: config.extensionTools ?? CONFIG_DEFAULTS.extensionTools,
58
+ signal,
59
+ sessionId,
60
+ onUpdate: (partial) => {
61
+ singleResult.messages = partial.messages;
62
+ singleResult.usage = partial.usage;
63
+ singleResult.model = partial.model;
64
+ singleResult.stopReason = partial.stopReason;
65
+ singleResult.errorMessage = partial.errorMessage;
66
+ if (onUpdate) onUpdate({
67
+ content: [{
68
+ type: "text",
69
+ text: getFinalOutput(partial.messages) || "(working...)"
70
+ }],
71
+ details: singleResult
72
+ });
73
+ }
74
+ });
75
+ singleResult.exitCode = result.exitCode;
76
+ singleResult.messages = result.messages;
77
+ singleResult.usage = result.usage;
78
+ singleResult.model = result.model;
79
+ singleResult.stopReason = result.stopReason;
80
+ singleResult.errorMessage = result.errorMessage;
81
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
82
+ const output = getFinalOutput(result.messages) || "(no output)";
83
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
84
+ return subAgentResult(output, singleResult);
85
+ },
86
+ renderCall(args, theme) {
87
+ const desc = args.description || "...";
88
+ const preview = desc.length > 80 ? `${desc.slice(0, 80)}...` : desc;
89
+ return new Text(theme.fg("toolTitle", theme.bold("Task ")) + theme.fg("dim", preview), 0, 0);
90
+ },
91
+ renderResult(result, { expanded }, theme) {
92
+ const details = result.details;
93
+ if (!details) {
94
+ const text = result.content[0];
95
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
96
+ }
97
+ const container = new Container();
98
+ renderAgentTree(details, container, expanded, theme, {
99
+ label: "Task",
100
+ header: "statusOnly"
101
+ });
102
+ return container;
103
+ }
104
+ };
105
+ }
106
+ function task_default(pi) {
107
+ const cfg = getExtensionConfig("@bds_pi/task", CONFIG_DEFAULTS);
108
+ pi.registerTool(withPromptPatch(createTaskTool({
109
+ builtinTools: cfg.builtinTools,
110
+ extensionTools: cfg.extensionTools
111
+ })));
112
+ }
113
+ //#endregion
114
+ export { createTaskTool, task_default as default };
@@ -0,0 +1,6 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/tool-harness/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): void;
5
+ //#endregion
6
+ export { export_default as default };
@@ -0,0 +1,37 @@
1
+ //#region packages/extensions/tool-harness/index.ts
2
+ /**
3
+ * backward-compat alias map: old names -> registered names.
4
+ */
5
+ const TOOL_ALIASES = {
6
+ glob: "find",
7
+ edit_file: "edit",
8
+ create_file: "write"
9
+ };
10
+ function resolveAliases(names) {
11
+ return names.map((name) => TOOL_ALIASES[name] ?? name);
12
+ }
13
+ function tool_harness_default(pi) {
14
+ const raw = process.env.PI_INCLUDE_TOOLS;
15
+ if (!raw) return;
16
+ if (raw === "NONE") {
17
+ const applyEmpty = () => pi.setActiveTools([]);
18
+ pi.on("session_start", async () => {
19
+ applyEmpty();
20
+ });
21
+ pi.on("before_agent_start", async () => {
22
+ applyEmpty();
23
+ });
24
+ return;
25
+ }
26
+ const allowed = resolveAliases(raw.split(",").map((t) => t.trim()).filter(Boolean));
27
+ if (allowed.length === 0) return;
28
+ const applyFilter = () => pi.setActiveTools(allowed);
29
+ pi.on("session_start", async () => {
30
+ applyFilter();
31
+ });
32
+ pi.on("before_agent_start", async () => {
33
+ applyFilter();
34
+ });
35
+ }
36
+ //#endregion
37
+ export { tool_harness_default as default };
@@ -0,0 +1,7 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/undo-edit/index.d.ts
4
+ declare function createUndoEditTool(): ToolDefinition;
5
+ declare function export_default(pi: ExtensionAPI): void;
6
+ //#endregion
7
+ export { createUndoEditTool, export_default as default };
@@ -0,0 +1,127 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed, osc8Link, textSection } from "../core/box-format.js";
3
+ import { withFileLock } from "../core/mutex.js";
4
+ import { resolveWithVariants } from "./read.js";
5
+ import { findLatestChange, revertChange, simpleDiff } from "../core/file-tracker.js";
6
+ import * as path from "node:path";
7
+ import { Text } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import * as os from "node:os";
10
+ //#region packages/extensions/undo-edit/index.ts
11
+ /**
12
+ * undo_edit tool — reverts the last edit made to a file.
13
+ *
14
+ * uses lib/file-tracker's disk-based change records to find and
15
+ * revert the most recent non-reverted change for a given file.
16
+ *
17
+ * branch awareness: scans the current session branch (via
18
+ * sessionManager.getBranch()) to extract tool call IDs, then
19
+ * only considers changes from those IDs. this prevents undoing
20
+ * edits from a different conversation branch.
21
+ *
22
+ * mutex-locked to prevent concurrent undo + edit on the same file.
23
+ */
24
+ const COLLAPSED_EXCERPTS = [{
25
+ focus: "head",
26
+ context: 3
27
+ }, {
28
+ focus: "tail",
29
+ context: 5
30
+ }];
31
+ /**
32
+ * extract tool call IDs from the current session branch.
33
+ * session entries with type "message" and role "assistant" contain
34
+ * tool_calls arrays. we collect all tool call IDs so findLatestChange
35
+ * can filter to only branch-visible changes.
36
+ *
37
+ * falls back to empty array if getBranch() isn't available (e.g.,
38
+ * running in a context where session tree access is restricted).
39
+ */
40
+ function getActiveToolCallIds(sessionManager) {
41
+ try {
42
+ const branch = sessionManager.getBranch?.();
43
+ if (!Array.isArray(branch)) return [];
44
+ const ids = [];
45
+ for (const entry of branch) {
46
+ if (entry.type !== "message") continue;
47
+ const msg = entry.message;
48
+ if (msg?.role !== "assistant") continue;
49
+ if (Array.isArray(msg.tool_calls)) {
50
+ for (const tc of msg.tool_calls) if (tc.id) ids.push(tc.id);
51
+ }
52
+ if (Array.isArray(msg.content)) {
53
+ for (const block of msg.content) if (block.type === "tool_use" && block.id) ids.push(block.id);
54
+ }
55
+ }
56
+ return ids;
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+ function createUndoEditTool() {
62
+ return {
63
+ name: "undo_edit",
64
+ label: "Undo Edit",
65
+ description: "Undo the last edit made to a file.\n\nThis command reverts the most recent edit made to the specified file.\nIt will restore the file to its state before the last edit was made.\n\nReturns a diff showing the changes that were undone.",
66
+ parameters: Type.Object({ path: Type.String({ description: "The absolute path to the file whose last edit should be undone (must be absolute, not relative)." }) }),
67
+ renderCall(args, theme) {
68
+ const filePath = args.path || "...";
69
+ const home = os.homedir();
70
+ const shortened = filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
71
+ const linked = filePath.startsWith("/") ? osc8Link(`file://${filePath}`, shortened) : shortened;
72
+ return new Text(theme.fg("toolTitle", theme.bold("Undo ")) + theme.fg("dim", linked), 0, 0);
73
+ },
74
+ renderResult(result, { expanded }, _theme) {
75
+ const content = result.content?.[0];
76
+ if (!content || content.type !== "text") return new Text("(no output)", 0, 0);
77
+ return boxRendererWindowed(() => [textSection(void 0, content.text)], {
78
+ collapsed: { excerpts: COLLAPSED_EXCERPTS },
79
+ expanded: {}
80
+ }, void 0, expanded);
81
+ },
82
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
83
+ const resolved = resolveWithVariants(params.path, ctx.cwd);
84
+ return withFileLock(resolved, async () => {
85
+ const sessionId = ctx.sessionManager.getSessionId();
86
+ const activeIds = getActiveToolCallIds(ctx.sessionManager);
87
+ if (activeIds.length === 0) return {
88
+ content: [{
89
+ type: "text",
90
+ text: "no edits found to undo (no tool calls in current branch)."
91
+ }],
92
+ isError: true
93
+ };
94
+ const latest = findLatestChange(sessionId, resolved, activeIds);
95
+ if (!latest) return {
96
+ content: [{
97
+ type: "text",
98
+ text: `no edits found to undo for ${path.basename(resolved)}.`
99
+ }],
100
+ isError: true
101
+ };
102
+ const reverted = revertChange(sessionId, latest.toolCallId, latest.change.id);
103
+ if (!reverted) return {
104
+ content: [{
105
+ type: "text",
106
+ text: `failed to revert — change may have already been undone.`
107
+ }],
108
+ isError: true
109
+ };
110
+ let result = simpleDiff(path.basename(resolved), reverted.after, reverted.before);
111
+ if (reverted.isNewFile) result += `\n\n(file was created by the reverted edit — file restored to empty)`;
112
+ return {
113
+ content: [{
114
+ type: "text",
115
+ text: result
116
+ }],
117
+ details: { header: resolved }
118
+ };
119
+ });
120
+ }
121
+ };
122
+ }
123
+ function undo_edit_default(pi) {
124
+ pi.registerTool(withPromptPatch(createUndoEditTool()));
125
+ }
126
+ //#endregion
127
+ export { createUndoEditTool, undo_edit_default as default };
@@ -0,0 +1,12 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/web-search/index.d.ts
4
+ type WebSearchExtConfig = {
5
+ defaultMaxResults: number;
6
+ endpoint: string;
7
+ curlTimeoutSecs: number;
8
+ };
9
+ declare function createWebSearchTool(config?: WebSearchExtConfig): ToolDefinition;
10
+ declare function export_default(pi: ExtensionAPI): void;
11
+ //#endregion
12
+ export { createWebSearchTool, export_default as default };