@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,277 @@
1
+ import { getExtensionConfig } from "../core/config.js";
2
+ import { resolvePrompt } from "../core/pi-spawn.js";
3
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { BorderedLoader, SessionManager, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
6
+ import { complete } from "@mariozechner/pi-ai";
7
+ //#region packages/extensions/handoff/index.ts
8
+ /**
9
+ * handoff extension — replace compaction with LLM-driven context transfer.
10
+ *
11
+ * at ~85% context usage, generates a focused handoff prompt via LLM,
12
+ * stages `/handoff` in the editor. user presses Enter → new session
13
+ * with curated context, agent starts working immediately.
14
+ *
15
+ * manual usage anytime:
16
+ * /handoff implement this for teams
17
+ * /handoff execute phase one of the plan
18
+ * /handoff check other places that need this fix
19
+ */
20
+ const CONFIG_DEFAULTS = {
21
+ threshold: .85,
22
+ model: {
23
+ provider: "openrouter",
24
+ id: "google/gemini-3-flash-preview"
25
+ },
26
+ promptFile: "",
27
+ promptString: ""
28
+ };
29
+ const MAX_RELEVANT_FILES = 10;
30
+ function parsePromptSections(content) {
31
+ const sections = {};
32
+ const parts = content.split("\n# ");
33
+ for (const part of parts) {
34
+ const nl = part.indexOf("\n");
35
+ if (nl === -1) continue;
36
+ const name = part.slice(0, nl).trim();
37
+ const body = part.slice(nl + 1).trim();
38
+ if (name) sections[name] = body;
39
+ }
40
+ return sections;
41
+ }
42
+ function extractToolCallArgs(response) {
43
+ const toolCall = response.content.find((c) => c.type === "toolCall" && "name" in c && c.name === "create_handoff_context");
44
+ if (!toolCall) return null;
45
+ const args = toolCall.arguments;
46
+ return {
47
+ relevantInformation: args.relevantInformation ?? "",
48
+ relevantFiles: (Array.isArray(args.relevantFiles) ? args.relevantFiles : []).slice(0, MAX_RELEVANT_FILES)
49
+ };
50
+ }
51
+ function assembleHandoffPrompt(sessionId, extraction, goal) {
52
+ const parts = [];
53
+ parts.push(`Continuing work from session ${sessionId}. Use read_session to retrieve details if needed.`);
54
+ if (extraction.relevantFiles.length > 0) parts.push(extraction.relevantFiles.map((f) => `@${f}`).join(" "));
55
+ if (extraction.relevantInformation) parts.push(extraction.relevantInformation);
56
+ parts.push(goal);
57
+ return parts.join("\n\n");
58
+ }
59
+ const PROVENANCE_PREFIX = "↳ handed off from: ";
60
+ const PROVENANCE_ELLIPSIS = "…";
61
+ function getParentDescription(parentPath, maxWidth) {
62
+ const budget = maxWidth - 19 - 1;
63
+ try {
64
+ const session = SessionManager.open(parentPath);
65
+ const name = session.getSessionName();
66
+ if (name) return name.length > budget ? name.slice(0, Math.max(0, budget)) + PROVENANCE_ELLIPSIS : name;
67
+ const firstUser = session.getBranch().find((e) => e.type === "message" && "content" in e.message && e.message.role === "user");
68
+ if (firstUser) {
69
+ const content = firstUser.message.content;
70
+ const text = (Array.isArray(content) ? content : []).filter((c) => typeof c === "object" && c !== null && c.type === "text").map((c) => c.text).join(" ").trim();
71
+ if (text) return text.length > budget ? text.slice(0, Math.max(0, budget)) + PROVENANCE_ELLIPSIS : text;
72
+ }
73
+ return session.getHeader()?.id?.slice(0, 8) ?? parentPath.split("/").pop() ?? "unknown";
74
+ } catch {
75
+ return parentPath.split("/").pop() ?? "unknown";
76
+ }
77
+ }
78
+ function showProvenance(ctx, parentPath) {
79
+ ctx.ui.setWidget("handoff-provenance", (_tui, theme) => ({
80
+ render(width) {
81
+ const desc = getParentDescription(parentPath, width);
82
+ const content = theme.fg("dim", "↳ ") + truncateToWidth(`${PROVENANCE_PREFIX.slice(2)}${desc}`, width);
83
+ const contentWidth = visibleWidth(content);
84
+ const pad = Math.max(0, width - contentWidth);
85
+ return [" ".repeat(pad) + content];
86
+ },
87
+ invalidate() {}
88
+ }));
89
+ }
90
+ function handoff_default(pi) {
91
+ const cfg = getExtensionConfig("@bds_pi/handoff", CONFIG_DEFAULTS);
92
+ const handoffSections = parsePromptSections(resolvePrompt(cfg.promptString, cfg.promptFile));
93
+ const HANDOFF_TOOL = {
94
+ name: "create_handoff_context",
95
+ description: handoffSections["tool-description"] || "Extract context for handoff",
96
+ parameters: Type.Object({
97
+ relevantInformation: Type.String({ description: handoffSections["field-relevant-information"] || "Extract relevant context" }),
98
+ relevantFiles: Type.Array(Type.String(), { description: handoffSections["field-relevant-files"] || "Relevant file paths" })
99
+ })
100
+ };
101
+ function buildExtractionPrompt(conversationText, goal) {
102
+ return `${conversationText}\n\n${handoffSections["extraction-prompt"] ?? ""}\n${goal}\n\nUse the create_handoff_context tool to extract relevant information and files.`;
103
+ }
104
+ let storedHandoffPrompt = null;
105
+ let handoffPending = false;
106
+ let parentSessionFile;
107
+ let generating = false;
108
+ /** resolve the dedicated handoff model, fall back to ctx.model */
109
+ function getHandoffModel(ctx) {
110
+ return ctx.modelRegistry.find(cfg.model.provider, cfg.model.id) ?? ctx.model;
111
+ }
112
+ async function generateHandoffPrompt(ctx, handoffModel, goal, signal) {
113
+ const messages = ctx.sessionManager.getBranch().filter((e) => e.type === "message").map((e) => e.message);
114
+ if (messages.length === 0) return null;
115
+ const conversationText = serializeConversation(convertToLlm(messages));
116
+ const sessionId = ctx.sessionManager.getSessionId();
117
+ const apiKey = await ctx.modelRegistry.getApiKey(handoffModel);
118
+ const response = await complete(handoffModel, {
119
+ messages: [{
120
+ role: "user",
121
+ content: [{
122
+ type: "text",
123
+ text: buildExtractionPrompt(conversationText, goal)
124
+ }],
125
+ timestamp: Date.now()
126
+ }],
127
+ tools: [HANDOFF_TOOL]
128
+ }, {
129
+ apiKey,
130
+ signal,
131
+ toolChoice: "any"
132
+ });
133
+ if (response.stopReason === "aborted") return null;
134
+ if (response.stopReason === "error") throw new Error(response.errorMessage ?? "API request failed");
135
+ const extraction = extractToolCallArgs(response);
136
+ if (!extraction) return null;
137
+ return assembleHandoffPrompt(sessionId, extraction, goal);
138
+ }
139
+ /** switch to a new session and send the handoff prompt */
140
+ async function executeHandoff(prompt, parent, ctx) {
141
+ storedHandoffPrompt = null;
142
+ handoffPending = false;
143
+ generating = false;
144
+ ctx.ui?.setStatus?.("handoff", "");
145
+ pi.events.emit("editor:remove-label", { key: "handoff" });
146
+ if ((await ctx.newSession({ parentSession: parent })).cancelled) return false;
147
+ if (parent) showProvenance(ctx, parent);
148
+ pi.sendUserMessage(prompt);
149
+ return true;
150
+ }
151
+ pi.on("session_start", async (_event, ctx) => {
152
+ const parentPath = ctx.sessionManager.getHeader()?.parentSession;
153
+ if (parentPath) showProvenance(ctx, parentPath);
154
+ });
155
+ pi.on("session_before_compact", async (_event, _ctx) => {
156
+ return { cancel: true };
157
+ });
158
+ pi.on("agent_end", async (_event, ctx) => {
159
+ if (handoffPending || generating) return;
160
+ const usage = ctx.getContextUsage();
161
+ if (!usage || usage.percent === null) return;
162
+ if (usage.percent < cfg.threshold * 100) return;
163
+ const handoffModel = getHandoffModel(ctx);
164
+ if (!handoffModel) return;
165
+ generating = true;
166
+ parentSessionFile = ctx.sessionManager.getSessionFile();
167
+ try {
168
+ const prompt = await generateHandoffPrompt(ctx, handoffModel, "continue the most specific pending task from the conversation");
169
+ if (!prompt) {
170
+ generating = false;
171
+ ctx.ui.notify("handoff generation failed: no extraction result", "error");
172
+ return;
173
+ }
174
+ storedHandoffPrompt = prompt;
175
+ handoffPending = true;
176
+ generating = false;
177
+ ctx.ui.setEditorText("/handoff");
178
+ ctx.ui.setStatus("handoff", `handoff ready (${Math.round(usage.percent)}%)`);
179
+ pi.events.emit("editor:set-label", {
180
+ key: "handoff",
181
+ text: `handoff ready (${Math.round(usage.percent)}%)`,
182
+ position: "top",
183
+ align: "right"
184
+ });
185
+ ctx.ui.notify(`context at ${Math.round(usage.percent)}% — handoff prompt generated. press enter to continue in a new session.`, "warning");
186
+ } catch (err) {
187
+ generating = false;
188
+ ctx.ui.notify(`handoff generation failed: ${String(err)}`, "error");
189
+ }
190
+ });
191
+ pi.registerCommand("handoff", {
192
+ description: "Transfer context to a new focused session (replaces compaction)",
193
+ handler: async (args, ctx) => {
194
+ const goal = args.trim();
195
+ if (goal && !handoffPending) {
196
+ const handoffModel = getHandoffModel(ctx);
197
+ if (!handoffModel) {
198
+ ctx.ui.notify("no model available for handoff", "error");
199
+ return;
200
+ }
201
+ parentSessionFile = ctx.sessionManager.getSessionFile();
202
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
203
+ const loader = new BorderedLoader(tui, theme, `generating handoff prompt (${handoffModel.name})...`);
204
+ loader.onAbort = () => done(null);
205
+ generateHandoffPrompt(ctx, handoffModel, goal, loader.signal).then(done).catch((err) => {
206
+ console.error("handoff generation failed:", err);
207
+ done(null);
208
+ });
209
+ return loader;
210
+ });
211
+ if (!result) {
212
+ ctx.ui.notify("cancelled", "info");
213
+ return;
214
+ }
215
+ storedHandoffPrompt = result;
216
+ }
217
+ if (!storedHandoffPrompt) {
218
+ ctx.ui.notify("no handoff prompt available. usage: /handoff <goal>", "error");
219
+ return;
220
+ }
221
+ const edited = await ctx.ui.editor("handoff prompt — ⏎ to handoff ␛ to cancel", storedHandoffPrompt);
222
+ if (!edited) {
223
+ ctx.ui.notify("handoff cancelled", "info");
224
+ return;
225
+ }
226
+ const prompt = edited;
227
+ if (!await executeHandoff(prompt, parentSessionFile, ctx)) {
228
+ storedHandoffPrompt = prompt;
229
+ handoffPending = true;
230
+ ctx.ui.notify("session switch cancelled", "info");
231
+ }
232
+ }
233
+ });
234
+ pi.on("session_switch", async (_event, ctx) => {
235
+ storedHandoffPrompt = null;
236
+ handoffPending = false;
237
+ generating = false;
238
+ pi.events.emit("editor:remove-label", { key: "handoff" });
239
+ ctx.ui.setWidget("handoff-provenance", void 0);
240
+ });
241
+ const handoffTool = {
242
+ name: "handoff",
243
+ label: "Handoff",
244
+ description: "Hand off to a new session. Generates a handoff prompt from the current conversation and stages /handoff in the editor. The user presses Enter to review the prompt, then confirms to switch sessions.",
245
+ promptSnippet: "Hand off to a new session with a generated context transfer prompt",
246
+ promptGuidelines: ["Use this when context is getting crowded or the user asks to continue in a fresh session.", "Set goal to a specific next task, not a vague continuation."],
247
+ parameters: Type.Object({ goal: Type.String({ description: "What should be accomplished in the new session. Be specific about the next task." }) }),
248
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
249
+ const p = params;
250
+ const handoffModel = getHandoffModel(_ctx);
251
+ if (!handoffModel) throw new Error("no model available for handoff extraction");
252
+ parentSessionFile = _ctx.sessionManager.getSessionFile();
253
+ const prompt = await generateHandoffPrompt(_ctx, handoffModel, p.goal, _signal ?? void 0);
254
+ if (!prompt) throw new Error("handoff generation failed: could not extract context");
255
+ storedHandoffPrompt = prompt;
256
+ handoffPending = true;
257
+ _ctx.ui.setEditorText("/handoff");
258
+ _ctx.ui.setStatus("handoff", "handoff ready");
259
+ pi.events.emit("editor:set-label", {
260
+ key: "handoff",
261
+ text: "handoff ready",
262
+ position: "top",
263
+ align: "right"
264
+ });
265
+ return {
266
+ content: [{
267
+ type: "text",
268
+ text: `handoff prompt generated for: "${p.goal}". staged /handoff — press Enter to continue in a new session.`
269
+ }],
270
+ details: void 0
271
+ };
272
+ }
273
+ };
274
+ pi.registerTool(handoffTool);
275
+ }
276
+ //#endregion
277
+ export { handoff_default as default };
@@ -0,0 +1,13 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/librarian/index.d.ts
4
+ interface LibrarianConfig {
5
+ systemPrompt?: string;
6
+ model?: string;
7
+ extensionTools?: string[];
8
+ builtinTools?: string[];
9
+ }
10
+ declare function createLibrarianTool(config?: LibrarianConfig): ToolDefinition;
11
+ declare function export_default(pi: ExtensionAPI): void;
12
+ //#endregion
13
+ export { LibrarianConfig, createLibrarianTool, export_default as default };
@@ -0,0 +1,113 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { getExtensionConfig } from "../core/config.js";
3
+ import { piSpawn, resolvePrompt, 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/librarian/index.ts
8
+ const CONFIG_DEFAULTS = {
9
+ model: "openrouter/anthropic/claude-haiku-4.5",
10
+ extensionTools: [
11
+ "read_github",
12
+ "search_github",
13
+ "list_directory_github",
14
+ "list_repositories",
15
+ "glob_github",
16
+ "commit_search",
17
+ "diff",
18
+ "web_search"
19
+ ],
20
+ builtinTools: [],
21
+ promptFile: "",
22
+ promptString: ""
23
+ };
24
+ function createLibrarianTool(config = {}) {
25
+ return {
26
+ name: "librarian",
27
+ label: "Librarian",
28
+ description: "The Librarian — a specialized codebase understanding agent that helps answer questions about large, complex codebases across GitHub repositories.\n\nThe Librarian reads from GitHub — it can see public repositories and private repositories you have access to via `gh` CLI auth.\n\nWHEN TO USE THE LIBRARIAN:\n- Understanding complex multi-repository codebases\n- Exploring relationships between different repositories\n- Analyzing architectural patterns across projects\n- Finding specific implementations across codebases\n- Understanding code evolution and commit history\n- Getting comprehensive explanations of how features work\n\nWHEN NOT TO USE THE LIBRARIAN:\n- Simple local file reading (use Read directly)\n- Local codebase searches (use finder)\n- Code modifications (use other tools)\n\nUSAGE GUIDELINES:\n- Be specific about what repositories or projects you want to understand\n- Provide context about what you're trying to achieve\n- The Librarian explores thoroughly before providing comprehensive answers\n- When getting an answer from the Librarian, show it to the user in full, do not summarize it.",
29
+ parameters: Type.Object({
30
+ query: Type.String({ description: "Your question about the codebase. Be specific about what you want to understand." }),
31
+ context: Type.Optional(Type.String({ description: "Optional context about what you're trying to achieve or background information." }))
32
+ }),
33
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
34
+ let sessionId = "";
35
+ try {
36
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
37
+ } catch {}
38
+ const p = params;
39
+ const parts = [p.query];
40
+ if (p.context) parts.push(`\nContext: ${p.context}`);
41
+ const fullTask = parts.join("\n");
42
+ const singleResult = {
43
+ agent: "librarian",
44
+ task: p.query,
45
+ exitCode: -1,
46
+ messages: [],
47
+ usage: zeroUsage()
48
+ };
49
+ const result = await piSpawn({
50
+ cwd: ctx.cwd,
51
+ task: fullTask,
52
+ model: config.model ?? CONFIG_DEFAULTS.model,
53
+ builtinTools: config.builtinTools ?? CONFIG_DEFAULTS.builtinTools,
54
+ extensionTools: config.extensionTools ?? CONFIG_DEFAULTS.extensionTools,
55
+ systemPromptBody: config.systemPrompt,
56
+ signal,
57
+ sessionId,
58
+ onUpdate: (partial) => {
59
+ singleResult.messages = partial.messages;
60
+ singleResult.usage = partial.usage;
61
+ singleResult.model = partial.model;
62
+ singleResult.stopReason = partial.stopReason;
63
+ singleResult.errorMessage = partial.errorMessage;
64
+ if (onUpdate) onUpdate({
65
+ content: [{
66
+ type: "text",
67
+ text: getFinalOutput(partial.messages) || "(exploring...)"
68
+ }],
69
+ details: singleResult
70
+ });
71
+ }
72
+ });
73
+ singleResult.exitCode = result.exitCode;
74
+ singleResult.messages = result.messages;
75
+ singleResult.usage = result.usage;
76
+ singleResult.model = result.model;
77
+ singleResult.stopReason = result.stopReason;
78
+ singleResult.errorMessage = result.errorMessage;
79
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
80
+ const output = getFinalOutput(result.messages) || "(no output)";
81
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
82
+ return subAgentResult(output, singleResult);
83
+ },
84
+ renderCall(args, theme) {
85
+ const preview = args.query ? args.query.length > 80 ? `${args.query.slice(0, 80)}...` : args.query : "...";
86
+ return new Text(theme.fg("toolTitle", theme.bold("librarian ")) + theme.fg("dim", preview), 0, 0);
87
+ },
88
+ renderResult(result, { expanded }, theme) {
89
+ const details = result.details;
90
+ if (!details) {
91
+ const text = result.content[0];
92
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
93
+ }
94
+ const container = new Container();
95
+ renderAgentTree(details, container, expanded, theme, {
96
+ label: "librarian",
97
+ header: "statusOnly"
98
+ });
99
+ return container;
100
+ }
101
+ };
102
+ }
103
+ function librarian_default(pi) {
104
+ const cfg = getExtensionConfig("@bds_pi/librarian", CONFIG_DEFAULTS);
105
+ pi.registerTool(withPromptPatch(createLibrarianTool({
106
+ systemPrompt: resolvePrompt(cfg.promptString, cfg.promptFile),
107
+ model: cfg.model,
108
+ extensionTools: cfg.extensionTools,
109
+ builtinTools: cfg.builtinTools
110
+ })));
111
+ }
112
+ //#endregion
113
+ export { createLibrarianTool, librarian_default as default };
@@ -0,0 +1,13 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/look-at/index.d.ts
4
+ interface LookAtConfig {
5
+ systemPrompt?: string;
6
+ model?: string;
7
+ extensionTools?: string[];
8
+ builtinTools?: string[];
9
+ }
10
+ declare function createLookAtTool(config?: LookAtConfig): ToolDefinition;
11
+ declare function export_default(pi: ExtensionAPI): void;
12
+ //#endregion
13
+ export { LookAtConfig, createLookAtTool, export_default as default };
@@ -0,0 +1,121 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { getExtensionConfig } from "../core/config.js";
3
+ import { piSpawn, resolvePrompt, 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/look-at/index.ts
8
+ const CONFIG_DEFAULTS = {
9
+ model: "openrouter/google/gemini-3-flash-preview",
10
+ extensionTools: ["read", "ls"],
11
+ builtinTools: ["read", "ls"],
12
+ promptFile: "",
13
+ promptString: ""
14
+ };
15
+ const DEFAULT_SYSTEM_PROMPT = `Analyze the provided file and answer the user's question about it. Be concise and direct, reference specific locations. When comparing files, systematically identify differences.`;
16
+ function createLookAtTool(config = {}) {
17
+ return {
18
+ name: "look_at",
19
+ label: "Look At",
20
+ description: "Extract specific information from a local file (including images and other media).\n\nUse this tool when you need to extract or summarize information from a file without getting the literal contents. Always provide a clear objective.\n\nPass reference files when you need to compare two or more things.\n\n## When to use this tool\n\n- Analyzing images that the Read tool cannot interpret\n- Extracting specific information or summaries from documents\n- Describing visual content in images or diagrams\n- When you only need analyzed/extracted data, not raw file contents\n\n## When NOT to use this tool\n\n- For source code or plain text files where you need exact contents — use Read instead\n- When you need to edit the file afterward (you need literal content from Read)\n- For simple file reading where no interpretation is needed",
21
+ parameters: Type.Object({
22
+ path: Type.String({ description: "Workspace-relative or absolute path to the file to analyze." }),
23
+ objective: Type.String({ description: "Natural-language description of the analysis goal (e.g., summarize, extract data, describe image)." }),
24
+ context: Type.String({ description: "The broader goal and context for the analysis." }),
25
+ referenceFiles: Type.Optional(Type.Array(Type.String(), { description: "Optional list of paths to reference files for comparison." }))
26
+ }),
27
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
28
+ const p = params;
29
+ let sessionId = "";
30
+ try {
31
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
32
+ } catch {}
33
+ const parts = [];
34
+ parts.push(`Read the file at "${p.path}" using the read tool.`);
35
+ if (p.referenceFiles && p.referenceFiles.length > 0) for (const ref of p.referenceFiles) parts.push(`Also read the reference file at "${ref}".`);
36
+ parts.push("");
37
+ parts.push(`Context: ${p.context}`);
38
+ parts.push("");
39
+ parts.push(`Analyze with this objective: ${p.objective}`);
40
+ if (p.referenceFiles && p.referenceFiles.length > 0) {
41
+ parts.push("");
42
+ parts.push("Compare the main file against the reference file(s). Identify all differences and similarities.");
43
+ }
44
+ const fullTask = parts.join("\n");
45
+ const singleResult = {
46
+ agent: "look_at",
47
+ task: p.objective,
48
+ exitCode: -1,
49
+ messages: [],
50
+ usage: zeroUsage()
51
+ };
52
+ const systemPrompt = config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
53
+ const result = await piSpawn({
54
+ cwd: ctx.cwd,
55
+ task: fullTask,
56
+ model: config.model ?? CONFIG_DEFAULTS.model,
57
+ builtinTools: config.builtinTools ?? CONFIG_DEFAULTS.builtinTools,
58
+ extensionTools: config.extensionTools ?? CONFIG_DEFAULTS.extensionTools,
59
+ systemPromptBody: systemPrompt,
60
+ signal,
61
+ sessionId,
62
+ onUpdate: (partial) => {
63
+ singleResult.messages = partial.messages;
64
+ singleResult.usage = partial.usage;
65
+ singleResult.model = partial.model;
66
+ singleResult.stopReason = partial.stopReason;
67
+ singleResult.errorMessage = partial.errorMessage;
68
+ if (onUpdate) onUpdate({
69
+ content: [{
70
+ type: "text",
71
+ text: getFinalOutput(partial.messages) || "(analyzing...)"
72
+ }],
73
+ details: singleResult
74
+ });
75
+ }
76
+ });
77
+ singleResult.exitCode = result.exitCode;
78
+ singleResult.messages = result.messages;
79
+ singleResult.usage = result.usage;
80
+ singleResult.model = result.model;
81
+ singleResult.stopReason = result.stopReason;
82
+ singleResult.errorMessage = result.errorMessage;
83
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
84
+ const output = getFinalOutput(result.messages) || "(no output)";
85
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
86
+ return subAgentResult(output, singleResult);
87
+ },
88
+ renderCall(args, theme) {
89
+ const path = args.path || "...";
90
+ const objective = args.objective ? args.objective.length > 60 ? `${args.objective.slice(0, 60)}...` : args.objective : "";
91
+ let text = theme.fg("toolTitle", theme.bold("look_at ")) + theme.fg("dim", path);
92
+ if (objective) text += theme.fg("muted", ` — ${objective}`);
93
+ if (args.referenceFiles?.length) text += theme.fg("muted", ` (+${args.referenceFiles.length} ref${args.referenceFiles.length > 1 ? "s" : ""})`);
94
+ return new Text(text, 0, 0);
95
+ },
96
+ renderResult(result, { expanded }, theme) {
97
+ const details = result.details;
98
+ if (!details) {
99
+ const text = result.content[0];
100
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
101
+ }
102
+ const container = new Container();
103
+ renderAgentTree(details, container, expanded, theme, {
104
+ label: "look_at",
105
+ header: "statusOnly"
106
+ });
107
+ return container;
108
+ }
109
+ };
110
+ }
111
+ function look_at_default(pi) {
112
+ const cfg = getExtensionConfig("@bds_pi/look-at", CONFIG_DEFAULTS);
113
+ pi.registerTool(withPromptPatch(createLookAtTool({
114
+ systemPrompt: resolvePrompt(cfg.promptString, cfg.promptFile),
115
+ model: cfg.model,
116
+ extensionTools: cfg.extensionTools,
117
+ builtinTools: cfg.builtinTools
118
+ })));
119
+ }
120
+ //#endregion
121
+ export { createLookAtTool, look_at_default as default };
@@ -0,0 +1,8 @@
1
+ import { ReadLimits } from "./read.js";
2
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
3
+
4
+ //#region packages/extensions/ls/index.d.ts
5
+ declare function createLsTool(limits: ReadLimits): ToolDefinition;
6
+ declare function export_default(pi: ExtensionAPI): void;
7
+ //#endregion
8
+ export { createLsTool, export_default as default };
@@ -0,0 +1,87 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed, osc8Link, textSection } from "../core/box-format.js";
3
+ import { NORMAL_LIMITS, listDirectory, resolveWithVariants } from "./read.js";
4
+ import * as fs from "node:fs";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ import { Type } from "@sinclair/typebox";
7
+ import * as os from "node:os";
8
+ //#region packages/extensions/ls/index.ts
9
+ /**
10
+ * ls tool shadow — redirects to read's directory listing.
11
+ *
12
+ * directory listing is part of Read.
13
+ * pi has a built-in ls tool that models may call by habit. this shadow
14
+ * does the listing (no wasted tool call) but steers the model toward
15
+ * using read for future calls.
16
+ */
17
+ const COLLAPSED_EXCERPTS = [{
18
+ focus: "head",
19
+ context: 3
20
+ }, {
21
+ focus: "tail",
22
+ context: 5
23
+ }];
24
+ function createLsTool(limits) {
25
+ return {
26
+ name: "ls",
27
+ label: "List Directory",
28
+ description: "List directory contents. Prefer using the read tool instead — it handles both files and directories.",
29
+ parameters: Type.Object({ path: Type.Optional(Type.String({ description: "The absolute path to the directory to list. Defaults to cwd." })) }),
30
+ renderCall(args, theme) {
31
+ const dirPath = args.path || ".";
32
+ const home = os.homedir();
33
+ const shortened = dirPath.startsWith(home) ? `~${dirPath.slice(home.length)}` : dirPath;
34
+ const linked = dirPath.startsWith("/") ? osc8Link(`file://${dirPath}`, shortened) : shortened;
35
+ return new Text(theme.fg("toolTitle", theme.bold("ls ")) + theme.fg("dim", linked), 0, 0);
36
+ },
37
+ renderResult(result, { expanded }, _theme) {
38
+ const content = result.content?.[0];
39
+ if (!content || content.type !== "text") return new Text("(no output)", 0, 0);
40
+ return boxRendererWindowed(() => [textSection(void 0, content.text)], {
41
+ collapsed: { excerpts: COLLAPSED_EXCERPTS },
42
+ expanded: {}
43
+ }, void 0, expanded);
44
+ },
45
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
46
+ const resolved = resolveWithVariants(params.path ?? ctx.cwd, ctx.cwd);
47
+ if (!fs.existsSync(resolved)) return {
48
+ content: [{
49
+ type: "text",
50
+ text: `directory not found: ${resolved}`
51
+ }],
52
+ isError: true
53
+ };
54
+ if (!fs.statSync(resolved).isDirectory()) return {
55
+ content: [{
56
+ type: "text",
57
+ text: `not a directory: ${resolved}. use the read tool for files.`
58
+ }],
59
+ isError: true
60
+ };
61
+ try {
62
+ let text = listDirectory(resolved, limits.maxDirEntries);
63
+ text += "\n\n(note: prefer the read tool for directory listing — it handles both files and directories.)";
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text
68
+ }],
69
+ details: { header: resolved }
70
+ };
71
+ } catch (err) {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: err.message
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ }
81
+ };
82
+ }
83
+ function ls_default(pi) {
84
+ pi.registerTool(withPromptPatch(createLsTool(NORMAL_LIMITS)));
85
+ }
86
+ //#endregion
87
+ export { createLsTool, ls_default as default };
@@ -0,0 +1,20 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/mermaid/index.d.ts
4
+ type DiagramEntry = {
5
+ id: string;
6
+ block: {
7
+ code: string;
8
+ blockIndex: number;
9
+ startLine: number;
10
+ endLine: number;
11
+ };
12
+ context: {
13
+ beforeLines: string[];
14
+ afterLines: string[];
15
+ };
16
+ source: "assistant" | "user" | "command";
17
+ };
18
+ declare function mermaidInlineExtension(pi: ExtensionAPI): void;
19
+ //#endregion
20
+ export { DiagramEntry, mermaidInlineExtension as default };
@@ -0,0 +1,2 @@
1
+ import { t as mermaidInlineExtension } from "../mermaid-C7xCVBrw.js";
2
+ export { mermaidInlineExtension as default };
@@ -0,0 +1,13 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/oracle/index.d.ts
4
+ interface OracleConfig {
5
+ systemPrompt?: string;
6
+ model?: string;
7
+ extensionTools?: string[];
8
+ builtinTools?: string[];
9
+ }
10
+ declare function createOracleTool(config?: OracleConfig): ToolDefinition;
11
+ declare function export_default(pi: ExtensionAPI): void;
12
+ //#endregion
13
+ export { OracleConfig, createOracleTool, export_default as default };