@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,143 @@
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 * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { Container, Text } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ //#region packages/extensions/oracle/index.ts
10
+ /**
11
+ * oracle tool — expert technical advisor via gpt-5.2 sub-agent.
12
+ *
13
+ * replaces the generic subagent(agent: "oracle", task: ...) pattern
14
+ * with a dedicated tool. the model calls
15
+ * oracle(task: "...", context?: "...", files?: [...]) directly.
16
+ *
17
+ * the oracle operates zero-shot: no follow-up questions, makes its
18
+ * final message comprehensive. only the last assistant message is
19
+ * returned to the parent agent.
20
+ *
21
+ * system prompt loaded from sops-decrypted prompts at init time.
22
+ */
23
+ const CONFIG_DEFAULTS = {
24
+ model: "openrouter/openai/gpt-5.2",
25
+ extensionTools: [
26
+ "read",
27
+ "grep",
28
+ "find",
29
+ "ls",
30
+ "bash"
31
+ ],
32
+ builtinTools: [
33
+ "read",
34
+ "grep",
35
+ "find",
36
+ "ls",
37
+ "bash"
38
+ ],
39
+ promptFile: "",
40
+ promptString: ""
41
+ };
42
+ function createOracleTool(config = {}) {
43
+ return {
44
+ name: "oracle",
45
+ label: "Oracle",
46
+ description: "Consult the oracle - an AI advisor powered by a reasoning model that can plan, review, and provide expert guidance.\n\nThe oracle has access to tools: Read, Grep, Find, ls, Bash.\n\nYou should consult the oracle for:\n- Code reviews and architecture feedback\n- Finding difficult bugs across many files\n- Planning complex implementations or refactors\n- Answering complex technical questions requiring deep reasoning\n- Providing an alternative point of view\n\nYou should NOT consult the oracle for:\n- File reads or simple keyword searches (use Read or Grep directly)\n- Codebase searches (use finder)\n- Basic code modifications (do it yourself or use Task)\n\nUsage guidelines:\n- Be specific about what you want reviewed, planned, or debugged\n- Provide relevant context. If you know which files are involved, list them.",
47
+ parameters: Type.Object({
48
+ task: Type.String({ description: "The task or question for the oracle. Be specific about what guidance you need." }),
49
+ context: Type.Optional(Type.String({ description: "Optional context about the current situation or background information." })),
50
+ files: Type.Optional(Type.Array(Type.String(), { description: "Optional file paths the oracle should examine." }))
51
+ }),
52
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
53
+ const p = params;
54
+ let sessionId = "";
55
+ try {
56
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
57
+ } catch {}
58
+ const parts = [p.task];
59
+ if (p.context) parts.push(`\nContext: ${p.context}`);
60
+ if (p.files && p.files.length > 0) for (const filePath of p.files) {
61
+ const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.cwd, filePath);
62
+ try {
63
+ const content = fs.readFileSync(resolved, "utf-8");
64
+ parts.push(`\nFile: ${filePath}\n\`\`\`\n${content}\n\`\`\``);
65
+ } catch {
66
+ parts.push(`\nFile: ${filePath} (could not read)`);
67
+ }
68
+ }
69
+ const fullTask = parts.join("\n");
70
+ const singleResult = {
71
+ agent: "oracle",
72
+ task: p.task,
73
+ exitCode: -1,
74
+ messages: [],
75
+ usage: zeroUsage()
76
+ };
77
+ const result = await piSpawn({
78
+ cwd: ctx.cwd,
79
+ task: fullTask,
80
+ model: config.model ?? CONFIG_DEFAULTS.model,
81
+ builtinTools: config.builtinTools ?? CONFIG_DEFAULTS.builtinTools,
82
+ extensionTools: config.extensionTools ?? CONFIG_DEFAULTS.extensionTools,
83
+ systemPromptBody: config.systemPrompt,
84
+ signal,
85
+ sessionId,
86
+ onUpdate: (partial) => {
87
+ singleResult.messages = partial.messages;
88
+ singleResult.usage = partial.usage;
89
+ singleResult.model = partial.model;
90
+ singleResult.stopReason = partial.stopReason;
91
+ singleResult.errorMessage = partial.errorMessage;
92
+ if (onUpdate) onUpdate({
93
+ content: [{
94
+ type: "text",
95
+ text: getFinalOutput(partial.messages) || "(thinking...)"
96
+ }],
97
+ details: singleResult
98
+ });
99
+ }
100
+ });
101
+ singleResult.exitCode = result.exitCode;
102
+ singleResult.messages = result.messages;
103
+ singleResult.usage = result.usage;
104
+ singleResult.model = result.model;
105
+ singleResult.stopReason = result.stopReason;
106
+ singleResult.errorMessage = result.errorMessage;
107
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
108
+ const output = getFinalOutput(result.messages) || "(no output)";
109
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
110
+ return subAgentResult(output, singleResult);
111
+ },
112
+ renderCall(args, theme) {
113
+ const preview = args.task ? args.task.length > 80 ? `${args.task.slice(0, 80)}...` : args.task : "...";
114
+ let text = theme.fg("toolTitle", theme.bold("oracle ")) + theme.fg("dim", preview);
115
+ if (args.files?.length) text += theme.fg("muted", ` (${args.files.length} file${args.files.length > 1 ? "s" : ""})`);
116
+ return new Text(text, 0, 0);
117
+ },
118
+ renderResult(result, { expanded }, theme) {
119
+ const details = result.details;
120
+ if (!details) {
121
+ const text = result.content[0];
122
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
123
+ }
124
+ const container = new Container();
125
+ renderAgentTree(details, container, expanded, theme, {
126
+ label: "oracle",
127
+ header: "statusOnly"
128
+ });
129
+ return container;
130
+ }
131
+ };
132
+ }
133
+ function oracle_default(pi) {
134
+ const cfg = getExtensionConfig("@bds_pi/oracle", CONFIG_DEFAULTS);
135
+ pi.registerTool(withPromptPatch(createOracleTool({
136
+ systemPrompt: resolvePrompt(cfg.promptString, cfg.promptFile),
137
+ model: cfg.model,
138
+ extensionTools: cfg.extensionTools,
139
+ builtinTools: cfg.builtinTools
140
+ })));
141
+ }
142
+ //#endregion
143
+ export { createOracleTool, oracle_default as default };
@@ -0,0 +1,13 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/read-session/index.d.ts
4
+ interface ReadSessionConfig {
5
+ systemPrompt?: string;
6
+ model?: string;
7
+ sessionsDir: string;
8
+ maxChars: number;
9
+ }
10
+ declare function createReadSessionTool(config: ReadSessionConfig): ToolDefinition;
11
+ declare function export_default(pi: ExtensionAPI): void;
12
+ //#endregion
13
+ export { ReadSessionConfig, createReadSessionTool, export_default as default };
@@ -0,0 +1,265 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { headTailChars } from "../core/output-buffer.js";
3
+ import { getExtensionConfig } from "../core/config.js";
4
+ import { piSpawn, zeroUsage } from "../core/pi-spawn.js";
5
+ import { getFinalOutput, renderAgentTree, subAgentResult } from "../core/sub-agent-render.js";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { Container, Text } from "@mariozechner/pi-tui";
9
+ import { Type } from "@sinclair/typebox";
10
+ import * as os from "node:os";
11
+ //#region packages/extensions/read-session/index.ts
12
+ /**
13
+ * read_session tool — extract relevant context from a pi session via sub-agent.
14
+ *
15
+ * loads a full session tree (all branches), renders it as structured markdown,
16
+ * then spawns a gemini flash sub-agent to extract only the information
17
+ * relevant to the stated goal. the agent sees the complete tree — including
18
+ * abandoned branches — so it can understand decision points and context.
19
+ *
20
+ * branch awareness: if a leaf_id is provided, the target branch is annotated
21
+ * in the rendered output so the agent knows which path to focus on.
22
+ */
23
+ const CONFIG_DEFAULTS = {
24
+ model: "openrouter/google/gemini-3-flash-preview",
25
+ sessionsDir: path.join(os.homedir(), ".pi", "agent", "sessions"),
26
+ maxChars: 12e4
27
+ };
28
+ const DEFAULT_SYSTEM_PROMPT = `You are analyzing a pi coding agent session transcript. Extract information relevant to the user's goal. Be specific — cite file paths, decisions made, code patterns discussed. If a specific branch is marked as the target, focus on that branch but use other branches for context about what was tried and abandoned.`;
29
+ function findSessionFile(sessionId, sessionsDir) {
30
+ if (!fs.existsSync(sessionsDir)) return null;
31
+ const walkDir = (dir) => {
32
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
33
+ const full = path.join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ const found = walkDir(full);
36
+ if (found) return found;
37
+ } else if (entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) return full;
38
+ }
39
+ return null;
40
+ };
41
+ let found = walkDir(sessionsDir);
42
+ if (found) return found;
43
+ const walkAndParse = (dir) => {
44
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
45
+ const full = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ const f = walkAndParse(full);
48
+ if (f) return f;
49
+ } else if (entry.name.endsWith(".jsonl")) try {
50
+ const firstLine = fs.readFileSync(full, "utf-8").split("\n")[0];
51
+ if (!firstLine) continue;
52
+ const header = JSON.parse(firstLine);
53
+ if (header.type === "session" && header.id === sessionId) return full;
54
+ } catch {}
55
+ }
56
+ return null;
57
+ };
58
+ return walkAndParse(sessionsDir);
59
+ }
60
+ function renderSessionTree(filePath, targetLeafId, maxChars) {
61
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
62
+ const entries = [];
63
+ let sessionId = "";
64
+ let sessionName = "";
65
+ let cwd = "";
66
+ let timestamp = "";
67
+ for (const line of lines) try {
68
+ const entry = JSON.parse(line);
69
+ if (entry.type === "session") {
70
+ sessionId = entry.id;
71
+ cwd = entry.cwd || "";
72
+ timestamp = entry.timestamp || "";
73
+ }
74
+ if (entry.type === "session_info" && entry.name) sessionName = entry.name;
75
+ if (entry.id) entries.push(entry);
76
+ } catch {}
77
+ const byId = /* @__PURE__ */ new Map();
78
+ const children = /* @__PURE__ */ new Map();
79
+ for (const e of entries) {
80
+ byId.set(e.id, e);
81
+ const parent = e.parentId ?? "__root__";
82
+ if (!children.has(parent)) children.set(parent, []);
83
+ children.get(parent).push(e.id);
84
+ }
85
+ const targetPath = /* @__PURE__ */ new Set();
86
+ if (targetLeafId) {
87
+ let current = targetLeafId;
88
+ while (current) {
89
+ targetPath.add(current);
90
+ const entry = byId.get(current);
91
+ if (!entry?.parentId) break;
92
+ current = entry.parentId;
93
+ }
94
+ }
95
+ const branchPoints = /* @__PURE__ */ new Set();
96
+ for (const [parentId, kids] of children.entries()) if (kids.length > 1) branchPoints.add(parentId);
97
+ const parts = [];
98
+ parts.push(`# session: ${sessionName || sessionId}`);
99
+ parts.push(`id: ${sessionId}`);
100
+ parts.push(`workspace: ${cwd}`);
101
+ parts.push(`started: ${timestamp}`);
102
+ if (targetLeafId) parts.push(`target branch leaf: ${targetLeafId}`);
103
+ parts.push("");
104
+ const renderEntry = (entryId, depth) => {
105
+ const entry = byId.get(entryId);
106
+ if (!entry) return;
107
+ const marker = targetPath.has(entryId) ? " [TARGET BRANCH]" : "";
108
+ const kids = children.get(entryId) || [];
109
+ if (kids.length > 1) parts.push(`\n--- branch point (${kids.length} paths) ---\n`);
110
+ if (entry.type === "message") {
111
+ const msg = entry.message;
112
+ if (!msg) return;
113
+ if (msg.role === "user") {
114
+ const textParts = msg.content?.filter((p) => p.type === "text").map((p) => p.text).join("\n") || "";
115
+ if (textParts) {
116
+ parts.push(`## user${marker}`);
117
+ parts.push(textParts);
118
+ parts.push("");
119
+ }
120
+ } else if (msg.role === "assistant") {
121
+ const textParts = [];
122
+ const toolCalls = [];
123
+ for (const part of msg.content || []) if (part.type === "text" && part.text) textParts.push(part.text);
124
+ else if (part.type === "toolCall") {
125
+ const args = part.arguments ? JSON.stringify(part.arguments).slice(0, 200) : "";
126
+ toolCalls.push(`${part.name}(${args})`);
127
+ }
128
+ if (textParts.length > 0 || toolCalls.length > 0) {
129
+ parts.push(`## assistant${marker}`);
130
+ if (textParts.length > 0) parts.push(textParts.join("\n"));
131
+ if (toolCalls.length > 0) parts.push(`\ntool calls: ${toolCalls.join(", ")}`);
132
+ parts.push("");
133
+ }
134
+ } else if (msg.role === "toolResult") {
135
+ const toolName = msg.toolName || "?";
136
+ const textContent = msg.content?.filter((p) => p.type === "text").map((p) => p.text).join("\n") || "";
137
+ const truncated = textContent.length > 500 ? `${textContent.slice(0, 500)}... (truncated)` : textContent;
138
+ if (truncated) {
139
+ parts.push(`### ${toolName} result${msg.isError ? " (ERROR)" : ""}${marker}`);
140
+ parts.push(truncated);
141
+ parts.push("");
142
+ }
143
+ }
144
+ } else if (entry.type === "model_change") parts.push(`*model changed to ${String(entry.modelId)}*\n`);
145
+ for (const childId of kids) renderEntry(childId, depth + 1);
146
+ };
147
+ const rootIds = children.get("__root__") || [];
148
+ for (const e of entries) if (e.parentId && !byId.has(e.parentId) && !rootIds.includes(e.id)) rootIds.push(e.id);
149
+ for (const rootId of rootIds) renderEntry(rootId, 0);
150
+ let markdown = parts.join("\n");
151
+ const truncated = headTailChars(markdown, maxChars);
152
+ if (truncated.truncated) markdown = truncated.text;
153
+ return {
154
+ markdown,
155
+ sessionName,
156
+ sessionId
157
+ };
158
+ }
159
+ function createReadSessionTool(config) {
160
+ return {
161
+ name: "read_session",
162
+ label: "Read Session",
163
+ description: "Read and extract relevant content from a past pi session.\n\nLoads the full session tree (all branches, including abandoned paths), then uses AI to extract only the information relevant to your stated goal. The AI sees the complete tree to understand decision points and context.\n\nUse `search_sessions` first to find session IDs and branch leaf IDs.\n\nWHEN TO USE:\n- Extracting context from a previous session\n- Understanding what was tried and decided in a past session\n- Continuing work from a prior session\n\nWHEN NOT TO USE:\n- Current session context (already available)\n- Finding sessions (use search_sessions first)",
164
+ parameters: Type.Object({
165
+ session_id: Type.String({ description: "The session ID to read (from search_sessions results)." }),
166
+ goal: Type.String({ description: "What information you're looking for. Be specific about what to extract." }),
167
+ leaf_id: Type.Optional(Type.String({ description: "Optional branch leaf ID to focus on. The AI will see all branches but prioritize the target branch." }))
168
+ }),
169
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
170
+ const p = params;
171
+ const sessionFile = findSessionFile(p.session_id, config.sessionsDir);
172
+ if (!sessionFile) return {
173
+ content: [{
174
+ type: "text",
175
+ text: `session not found: ${p.session_id}`
176
+ }],
177
+ isError: true
178
+ };
179
+ const { markdown } = renderSessionTree(sessionFile, p.leaf_id, config.maxChars);
180
+ if (!markdown.trim()) return { content: [{
181
+ type: "text",
182
+ text: "(session is empty)"
183
+ }] };
184
+ let sessionId = "";
185
+ try {
186
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
187
+ } catch {}
188
+ const task = `Here is a pi coding agent session transcript:\n\n${markdown}\n\n---\n\nExtract the information relevant to this goal: ${p.goal}`;
189
+ const singleResult = {
190
+ agent: "read_session",
191
+ task: p.goal,
192
+ exitCode: -1,
193
+ messages: [],
194
+ usage: zeroUsage()
195
+ };
196
+ const systemPrompt = config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
197
+ const result = await piSpawn({
198
+ cwd: ctx.cwd,
199
+ task,
200
+ model: config.model ?? CONFIG_DEFAULTS.model,
201
+ builtinTools: [],
202
+ extensionTools: [],
203
+ systemPromptBody: systemPrompt,
204
+ signal,
205
+ sessionId,
206
+ onUpdate: (partial) => {
207
+ singleResult.messages = partial.messages;
208
+ singleResult.usage = partial.usage;
209
+ singleResult.model = partial.model;
210
+ singleResult.stopReason = partial.stopReason;
211
+ singleResult.errorMessage = partial.errorMessage;
212
+ if (onUpdate) onUpdate({
213
+ content: [{
214
+ type: "text",
215
+ text: getFinalOutput(partial.messages) || "(reading session...)"
216
+ }],
217
+ details: singleResult
218
+ });
219
+ }
220
+ });
221
+ singleResult.exitCode = result.exitCode;
222
+ singleResult.messages = result.messages;
223
+ singleResult.usage = result.usage;
224
+ singleResult.model = result.model;
225
+ singleResult.stopReason = result.stopReason;
226
+ singleResult.errorMessage = result.errorMessage;
227
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
228
+ const output = getFinalOutput(result.messages) || "(no output)";
229
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
230
+ return subAgentResult(output, singleResult);
231
+ },
232
+ renderCall(args, theme) {
233
+ const goal = args.goal ? args.goal.length > 60 ? `${args.goal.slice(0, 60)}...` : args.goal : "...";
234
+ let text = theme.fg("toolTitle", theme.bold("read_session ")) + theme.fg("dim", goal);
235
+ if (args.session_id) {
236
+ const shortId = args.session_id.length > 8 ? args.session_id.slice(0, 8) : args.session_id;
237
+ text += theme.fg("muted", ` (${shortId}...)`);
238
+ }
239
+ return new Text(text, 0, 0);
240
+ },
241
+ renderResult(result, { expanded }, theme) {
242
+ const details = result.details;
243
+ if (!details) {
244
+ const text = result.content[0];
245
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
246
+ }
247
+ const container = new Container();
248
+ renderAgentTree(details, container, expanded, theme, {
249
+ label: "read_session",
250
+ header: "statusOnly"
251
+ });
252
+ return container;
253
+ }
254
+ };
255
+ }
256
+ function read_session_default(pi) {
257
+ const cfg = getExtensionConfig("@bds_pi/read-session", CONFIG_DEFAULTS);
258
+ pi.registerTool(withPromptPatch(createReadSessionTool({
259
+ model: cfg.model,
260
+ sessionsDir: cfg.sessionsDir,
261
+ maxChars: cfg.maxChars
262
+ })));
263
+ }
264
+ //#endregion
265
+ export { createReadSessionTool, read_session_default as default };
@@ -0,0 +1,11 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/read-web-page/index.d.ts
4
+ interface ReadWebPageConfig {
5
+ systemPrompt?: string;
6
+ model?: string;
7
+ }
8
+ declare function createReadWebPageTool(config?: ReadWebPageConfig): ToolDefinition;
9
+ declare function export_default(pi: ExtensionAPI): void;
10
+ //#endregion
11
+ export { ReadWebPageConfig, createReadWebPageTool, export_default as default };
@@ -0,0 +1,234 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { osc8Link } from "../core/box-format.js";
3
+ import { OutputBuffer, headTailChars } from "../core/output-buffer.js";
4
+ import { getExtensionConfig } from "../core/config.js";
5
+ import { piSpawn, resolvePrompt, zeroUsage } from "../core/pi-spawn.js";
6
+ import { getFinalOutput, renderAgentTree, subAgentResult } from "../core/sub-agent-render.js";
7
+ import { htmlToMarkdown } from "../core/html-to-md.js";
8
+ import { spawn } from "node:child_process";
9
+ import { Container, Text } from "@mariozechner/pi-tui";
10
+ import { Type } from "@sinclair/typebox";
11
+ //#region packages/extensions/read-web-page/index.ts
12
+ /**
13
+ * v3: cheerio HTML→markdown + LLM Q&A + pagination + raw mode.
14
+ *
15
+ * cheerio strips chrome (nav, footer, scripts), finds main content area,
16
+ * converts to clean markdown. ~95% size reduction on typical pages.
17
+ *
18
+ * `prompt` spawns a gemini flash sub-agent that receives page content
19
+ * and returns AI-generated prose (36/1202 calls use this pattern).
20
+ * `start_index`/`max_length` provide character-level pagination (~16 calls).
21
+ * `raw` skips conversion entirely (1 call).
22
+ */
23
+ const HEAD_LINES = 500;
24
+ const TAIL_LINES = 500;
25
+ const MAX_CHARS = 64e3;
26
+ const CURL_TIMEOUT_SECS = 30;
27
+ const MAX_REDIRECTS = 5;
28
+ const CONFIG_DEFAULTS = {
29
+ model: "openrouter/google/gemini-3-flash-preview",
30
+ promptFile: "",
31
+ promptString: ""
32
+ };
33
+ const DEFAULT_PROMPT_SYSTEM = `Analyze web page content and answer questions. Be concise, answer from provided content only. No filler.`;
34
+ function fetchUrl(url, signal) {
35
+ return new Promise((resolve) => {
36
+ const child = spawn("curl", [
37
+ "-sL",
38
+ "-H",
39
+ "Accept: text/markdown, text/html;q=0.9",
40
+ "-m",
41
+ String(CURL_TIMEOUT_SECS),
42
+ "--max-redirs",
43
+ String(MAX_REDIRECTS),
44
+ "-A",
45
+ "Mozilla/5.0 (compatible; pi-agent/1.0)",
46
+ url
47
+ ], { stdio: [
48
+ "ignore",
49
+ "pipe",
50
+ "pipe"
51
+ ] });
52
+ const output = new OutputBuffer(HEAD_LINES, TAIL_LINES);
53
+ let stderr = "";
54
+ let aborted = false;
55
+ const onAbort = () => {
56
+ aborted = true;
57
+ if (!child.killed) child.kill("SIGTERM");
58
+ };
59
+ if (signal) if (signal.aborted) onAbort();
60
+ else signal.addEventListener("abort", onAbort, { once: true });
61
+ child.stdout?.on("data", (data) => {
62
+ output.add(data.toString("utf-8"));
63
+ });
64
+ child.stderr?.on("data", (data) => {
65
+ stderr += data.toString("utf-8");
66
+ });
67
+ child.on("error", (err) => {
68
+ signal?.removeEventListener("abort", onAbort);
69
+ resolve({
70
+ html: "",
71
+ error: `curl error: ${err.message}`
72
+ });
73
+ });
74
+ child.on("close", (code) => {
75
+ signal?.removeEventListener("abort", onAbort);
76
+ if (aborted) {
77
+ resolve({
78
+ html: "",
79
+ error: "fetch aborted"
80
+ });
81
+ return;
82
+ }
83
+ if (code !== 0) {
84
+ resolve({
85
+ html: "",
86
+ error: `fetch failed: ${stderr.trim() || `curl exited with code ${code}`}`
87
+ });
88
+ return;
89
+ }
90
+ const { text } = output.format();
91
+ resolve({ html: text });
92
+ });
93
+ });
94
+ }
95
+ function createReadWebPageTool(config = {}) {
96
+ return {
97
+ name: "read_web_page",
98
+ label: "Read Web Page",
99
+ description: "Read the contents of a web page at a given URL.\n\nReturns the page content converted to Markdown.\n\nWhen an objective is provided, it returns excerpts relevant to that objective.\n\nDo NOT use for localhost or local URLs — use `curl` via Bash instead.",
100
+ parameters: Type.Object({
101
+ url: Type.String({ description: "The URL of the web page to read." }),
102
+ objective: Type.Optional(Type.String({ description: "A natural-language description of the research goal. If set, only relevant excerpts will be returned. If not set, the full content is returned." })),
103
+ prompt: Type.Optional(Type.String({ description: "A question to answer about the page content. Spawns an AI sub-agent that reads the page and returns a prose answer." })),
104
+ start_index: Type.Optional(Type.Number({ description: "Character offset to start from in the converted content (for pagination)." })),
105
+ max_length: Type.Optional(Type.Number({ description: "Maximum number of characters to return (for pagination)." })),
106
+ raw: Type.Optional(Type.Boolean({ description: "Return raw HTML instead of converting to Markdown." })),
107
+ forceRefetch: Type.Optional(Type.Boolean({ description: "Force a live fetch (no caching). Currently always fetches live." }))
108
+ }),
109
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
110
+ const p = params;
111
+ const url = p.url;
112
+ if (!url.startsWith("http://") && !url.startsWith("https://")) return {
113
+ content: [{
114
+ type: "text",
115
+ text: `invalid URL: "${url}" — must start with http:// or https://`
116
+ }],
117
+ isError: true
118
+ };
119
+ const { html, error } = await fetchUrl(url, signal);
120
+ if (error) return {
121
+ content: [{
122
+ type: "text",
123
+ text: error
124
+ }],
125
+ isError: true
126
+ };
127
+ if (!html.trim()) return { content: [{
128
+ type: "text",
129
+ text: "(empty response)"
130
+ }] };
131
+ if (p.raw) return { content: [{
132
+ type: "text",
133
+ text: headTailChars(`Raw HTML content as requested:\n${html}`, MAX_CHARS).text
134
+ }] };
135
+ let content = htmlToMarkdown(html) ?? html;
136
+ if (p.start_index !== void 0 || p.max_length !== void 0) {
137
+ const total = content.length;
138
+ const start = p.start_index ?? 0;
139
+ const end = p.max_length !== void 0 ? start + p.max_length : total;
140
+ content = content.slice(start, end);
141
+ content += `\n\n[${start}–${Math.min(end, total)} of ${total} characters]`;
142
+ }
143
+ content = headTailChars(content, MAX_CHARS).text;
144
+ if (p.objective) content = `Objective: ${p.objective}\n\n---\n\n${content}`;
145
+ if (p.prompt) {
146
+ let sessionId = "";
147
+ try {
148
+ sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
149
+ } catch {}
150
+ const task = `Here is the content of ${url}:\n\n${content}\n\n---\n\nAnswer this question: ${p.prompt}`;
151
+ const singleResult = {
152
+ agent: "read_web_page",
153
+ task: p.prompt,
154
+ exitCode: -1,
155
+ messages: [],
156
+ usage: zeroUsage()
157
+ };
158
+ const promptSystem = config.systemPrompt || DEFAULT_PROMPT_SYSTEM;
159
+ const result = await piSpawn({
160
+ cwd: ctx.cwd,
161
+ task,
162
+ model: config.model ?? CONFIG_DEFAULTS.model,
163
+ builtinTools: [],
164
+ extensionTools: [],
165
+ systemPromptBody: promptSystem,
166
+ signal,
167
+ sessionId,
168
+ onUpdate: (partial) => {
169
+ singleResult.messages = partial.messages;
170
+ singleResult.usage = partial.usage;
171
+ singleResult.model = partial.model;
172
+ singleResult.stopReason = partial.stopReason;
173
+ singleResult.errorMessage = partial.errorMessage;
174
+ if (onUpdate) onUpdate({
175
+ content: [{
176
+ type: "text",
177
+ text: getFinalOutput(partial.messages) || "(analyzing...)"
178
+ }],
179
+ details: singleResult
180
+ });
181
+ }
182
+ });
183
+ singleResult.exitCode = result.exitCode;
184
+ singleResult.messages = result.messages;
185
+ singleResult.usage = result.usage;
186
+ singleResult.model = result.model;
187
+ singleResult.stopReason = result.stopReason;
188
+ singleResult.errorMessage = result.errorMessage;
189
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
190
+ const output = getFinalOutput(result.messages) || "(no output)";
191
+ if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
192
+ return subAgentResult(output, singleResult);
193
+ }
194
+ return { content: [{
195
+ type: "text",
196
+ text: content
197
+ }] };
198
+ },
199
+ renderCall(args, theme) {
200
+ const url = args.url || "...";
201
+ const displayUrl = url.length > 60 ? `${url.slice(0, 60)}...` : url;
202
+ const linkedUrl = url.startsWith("http") ? osc8Link(url, displayUrl) : displayUrl;
203
+ let text = theme.fg("toolTitle", theme.bold("read_web_page ")) + theme.fg("dim", linkedUrl);
204
+ const label = args.prompt || args.objective;
205
+ if (label) {
206
+ const short = label.length > 40 ? `${label.slice(0, 40)}...` : label;
207
+ text += theme.fg("muted", ` — ${short}`);
208
+ }
209
+ return new Text(text, 0, 0);
210
+ },
211
+ renderResult(result, { expanded }, theme) {
212
+ const details = result.details;
213
+ if (!details) {
214
+ const text = result.content[0];
215
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
216
+ }
217
+ const container = new Container();
218
+ renderAgentTree(details, container, expanded, theme, {
219
+ label: "read_web_page",
220
+ header: "statusOnly"
221
+ });
222
+ return container;
223
+ }
224
+ };
225
+ }
226
+ function read_web_page_default(pi) {
227
+ const cfg = getExtensionConfig("@bds_pi/read-web-page", CONFIG_DEFAULTS);
228
+ pi.registerTool(withPromptPatch(createReadWebPageTool({
229
+ systemPrompt: resolvePrompt(cfg.promptString, cfg.promptFile),
230
+ model: cfg.model
231
+ })));
232
+ }
233
+ //#endregion
234
+ export { createReadWebPageTool, read_web_page_default as default };