@oh-my-pi/pi-coding-agent 8.4.0 → 8.4.2

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 (92) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +6 -6
  3. package/scripts/format-prompts.ts +65 -23
  4. package/src/commit/agentic/prompts/session-user.md +0 -1
  5. package/src/commit/agentic/prompts/split-confirm.md +1 -1
  6. package/src/commit/agentic/prompts/system.md +1 -1
  7. package/src/commit/prompts/analysis-system.md +23 -26
  8. package/src/commit/prompts/analysis-user.md +1 -1
  9. package/src/commit/prompts/changelog-system.md +1 -2
  10. package/src/commit/prompts/changelog-user.md +1 -2
  11. package/src/commit/prompts/file-observer-system.md +1 -3
  12. package/src/commit/prompts/file-observer-user.md +1 -2
  13. package/src/commit/prompts/reduce-system.md +16 -16
  14. package/src/commit/prompts/reduce-user.md +1 -1
  15. package/src/commit/prompts/summary-retry.md +1 -2
  16. package/src/commit/prompts/summary-system.md +10 -10
  17. package/src/commit/prompts/summary-user.md +1 -1
  18. package/src/commit/prompts/types-description.md +1 -1
  19. package/src/config/keybindings.ts +3 -0
  20. package/src/config/settings-manager.ts +5 -0
  21. package/src/internal-urls/index.ts +1 -0
  22. package/src/internal-urls/plan-protocol.ts +95 -0
  23. package/src/modes/components/status-line/presets.ts +7 -7
  24. package/src/modes/components/status-line/segments.ts +16 -0
  25. package/src/modes/components/status-line/types.ts +4 -0
  26. package/src/modes/components/status-line-segment-editor.ts +1 -0
  27. package/src/modes/components/status-line.ts +16 -2
  28. package/src/modes/controllers/command-controller.ts +42 -0
  29. package/src/modes/controllers/event-controller.ts +13 -0
  30. package/src/modes/controllers/input-controller.ts +16 -0
  31. package/src/modes/interactive-mode.ts +219 -1
  32. package/src/modes/theme/theme.ts +7 -0
  33. package/src/modes/types.ts +7 -0
  34. package/src/patch/index.ts +9 -3
  35. package/src/plan-mode/state.ts +6 -0
  36. package/src/prompts/agents/explore.md +1 -1
  37. package/src/prompts/agents/frontmatter.md +1 -1
  38. package/src/prompts/agents/init.md +1 -1
  39. package/src/prompts/agents/plan.md +33 -49
  40. package/src/prompts/agents/reviewer.md +7 -7
  41. package/src/prompts/agents/task.md +1 -2
  42. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  43. package/src/prompts/compaction/branch-summary.md +3 -1
  44. package/src/prompts/compaction/compaction-summary.md +3 -1
  45. package/src/prompts/compaction/compaction-turn-prefix.md +2 -1
  46. package/src/prompts/compaction/compaction-update-summary.md +3 -1
  47. package/src/prompts/review-request.md +4 -1
  48. package/src/prompts/system/custom-system-prompt.md +8 -8
  49. package/src/prompts/system/file-operations.md +1 -1
  50. package/src/prompts/system/plan-mode-active.md +113 -0
  51. package/src/prompts/system/plan-mode-approved.md +16 -0
  52. package/src/prompts/system/plan-mode-reference.md +14 -0
  53. package/src/prompts/system/plan-mode-subagent.md +36 -0
  54. package/src/prompts/system/summarization-system.md +1 -1
  55. package/src/prompts/system/system-prompt.md +17 -27
  56. package/src/prompts/system/title-system.md +1 -1
  57. package/src/prompts/system/ttsr-interrupt.md +1 -1
  58. package/src/prompts/system/web-search.md +1 -1
  59. package/src/prompts/tools/ask.md +1 -3
  60. package/src/prompts/tools/bash.md +1 -1
  61. package/src/prompts/tools/calculator.md +1 -1
  62. package/src/prompts/tools/enter-plan-mode.md +92 -0
  63. package/src/prompts/tools/exit-plan-mode.md +38 -0
  64. package/src/prompts/tools/fetch.md +1 -1
  65. package/src/prompts/tools/find.md +1 -1
  66. package/src/prompts/tools/gemini-image.md +1 -1
  67. package/src/prompts/tools/grep.md +1 -1
  68. package/src/prompts/tools/lsp.md +1 -1
  69. package/src/prompts/tools/patch.md +1 -3
  70. package/src/prompts/tools/python.md +2 -4
  71. package/src/prompts/tools/read.md +1 -1
  72. package/src/prompts/tools/replace.md +16 -16
  73. package/src/prompts/tools/ssh.md +1 -4
  74. package/src/prompts/tools/task.md +1 -3
  75. package/src/prompts/tools/todo-write.md +13 -16
  76. package/src/prompts/tools/web-search.md +1 -1
  77. package/src/prompts/tools/write.md +1 -1
  78. package/src/sdk.ts +61 -10
  79. package/src/session/agent-session.ts +267 -0
  80. package/src/task/executor.ts +1 -0
  81. package/src/task/index.ts +18 -4
  82. package/src/tools/enter-plan-mode.ts +76 -0
  83. package/src/tools/exit-plan-mode.ts +62 -0
  84. package/src/tools/find.ts +5 -2
  85. package/src/tools/grep.ts +13 -12
  86. package/src/tools/index.ts +19 -1
  87. package/src/tools/plan-mode-guard.ts +46 -0
  88. package/src/tools/read.ts +8 -4
  89. package/src/tools/write.ts +3 -2
  90. package/src/utils/tools-manager.ts +38 -9
  91. package/src/web/search/providers/perplexity.ts +3 -1
  92. package/src/web/search/types.ts +3 -1
package/src/tools/grep.ts CHANGED
@@ -107,21 +107,17 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
107
107
  private readonly session: ToolSession;
108
108
  private readonly ops: GrepOperations;
109
109
 
110
- private readonly rgPath: Promise<string | undefined>;
111
-
112
110
  constructor(session: ToolSession, options?: GrepToolOptions) {
113
111
  this.session = session;
114
112
  this.ops = options?.operations ?? defaultGrepOperations;
115
113
  this.description = renderPromptTemplate(grepDescription);
116
- this.rgPath = ensureTool("rg", true);
117
114
  }
118
115
 
119
116
  /**
120
117
  * Validates a pattern against ripgrep's regex engine.
121
118
  * Uses a quick dry-run against /dev/null to check for parse errors.
122
119
  */
123
- private async validateRegexPattern(pattern: string): Promise<{ valid: boolean; error?: string }> {
124
- const rgPath = await this.rgPath;
120
+ private async validateRegexPattern(pattern: string, rgPath?: string): Promise<{ valid: boolean; error?: string }> {
125
121
  if (!rgPath) {
126
122
  return { valid: true }; // Can't validate, assume valid
127
123
  }
@@ -146,7 +142,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
146
142
  params: GrepParams,
147
143
  signal?: AbortSignal,
148
144
  _onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
149
- _context?: AgentToolContext,
145
+ toolContext?: AgentToolContext,
150
146
  ): Promise<AgentToolResult<GrepToolDetails>> {
151
147
  const {
152
148
  pattern,
@@ -167,19 +163,24 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
167
163
  return untilAborted(signal, async () => {
168
164
  // Auto-detect invalid regex patterns and switch to literal mode
169
165
  // This handles cases like "abort(" which would cause ripgrep regex parse errors
166
+ const rgPath = await ensureTool("rg", {
167
+ silent: true,
168
+ notify: message => toolContext?.ui?.notify(message, "info"),
169
+ });
170
+
171
+ if (!rgPath) {
172
+ throw new ToolError("rg is not available and could not be downloaded");
173
+ }
174
+
170
175
  let useLiteral = literal ?? false;
171
176
  if (!useLiteral) {
172
- const validation = await this.validateRegexPattern(pattern);
177
+ const validation = await this.validateRegexPattern(pattern, rgPath);
173
178
  if (!validation.valid) {
174
179
  useLiteral = true;
175
180
  }
176
181
  }
177
182
 
178
- const rgPath = await this.rgPath;
179
- if (!rgPath) {
180
- throw new ToolError("ripgrep (rg) is not available and could not be downloaded");
181
- }
182
-
183
+ // rgPath resolved earlier
183
184
  const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
184
185
  const scopePath = (() => {
185
186
  const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
@@ -8,6 +8,7 @@ import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
8
8
  import { checkPythonKernelAvailability } from "../ipy/kernel";
9
9
  import { LspTool } from "../lsp";
10
10
  import { EditTool } from "../patch";
11
+ import type { PlanModeState } from "../plan-mode/state";
11
12
  import type { ArtifactManager } from "../session/artifacts";
12
13
  import { TaskTool } from "../task";
13
14
  import type { AgentOutputManager } from "../task/output-manager";
@@ -18,6 +19,8 @@ import { AskTool } from "./ask";
18
19
  import { BashTool } from "./bash";
19
20
  import { CalculatorTool } from "./calculator";
20
21
  import { CompleteTool } from "./complete";
22
+ import { EnterPlanModeTool } from "./enter-plan-mode";
23
+ import { ExitPlanModeTool } from "./exit-plan-mode";
21
24
  import { FetchTool } from "./fetch";
22
25
  import { FindTool } from "./find";
23
26
  import { GrepTool } from "./grep";
@@ -70,6 +73,8 @@ export { AskTool, type AskToolDetails } from "./ask";
70
73
  export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
71
74
  export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
72
75
  export { CompleteTool } from "./complete";
76
+ export { type EnterPlanModeDetails, EnterPlanModeTool } from "./enter-plan-mode";
77
+ export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
73
78
  export { FetchTool, type FetchToolDetails } from "./fetch";
74
79
  export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
75
80
  export { setPreferredImageProvider } from "./gemini-image";
@@ -126,6 +131,8 @@ export interface ToolSession {
126
131
  requireCompleteTool?: boolean;
127
132
  /** Get session file */
128
133
  getSessionFile: () => string | null;
134
+ /** Get session ID */
135
+ getSessionId?: () => string | null;
129
136
  /** Cached artifact manager (allocated per ToolSession) */
130
137
  artifactManager?: ArtifactManager;
131
138
  /** Get artifacts directory for artifact:// URLs and $ARTIFACTS env var */
@@ -147,7 +154,10 @@ export interface ToolSession {
147
154
  /** Agent output manager for unique agent:// IDs across task invocations */
148
155
  agentOutputManager?: AgentOutputManager;
149
156
  /** Settings manager for passing to subagents (avoids SQLite access in workers) */
150
- settingsManager?: { serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings };
157
+ settingsManager?: {
158
+ serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings;
159
+ getPlansDirectory: (cwd?: string) => string;
160
+ };
151
161
  /** Settings manager (optional) */
152
162
  settings?: {
153
163
  getImageAutoResize(): boolean;
@@ -165,6 +175,8 @@ export interface ToolSession {
165
175
  getPythonKernelMode?(): "session" | "per-call";
166
176
  getPythonSharedGateway?(): boolean;
167
177
  };
178
+ /** Plan mode state (if active) */
179
+ getPlanModeState?: () => PlanModeState | undefined;
168
180
  }
169
181
 
170
182
  type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
@@ -187,11 +199,13 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
187
199
  fetch: s => new FetchTool(s),
188
200
  web_search: s => new WebSearchTool(s),
189
201
  write: s => new WriteTool(s),
202
+ enter_plan_mode: s => new EnterPlanModeTool(s),
190
203
  };
191
204
 
192
205
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
193
206
  complete: s => new CompleteTool(s),
194
207
  report_finding: () => reportFindingTool,
208
+ exit_plan_mode: s => new ExitPlanModeTool(s),
195
209
  };
196
210
 
197
211
  export type ToolName = keyof typeof BUILTIN_TOOLS;
@@ -234,6 +248,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
234
248
  const includeComplete = session.requireCompleteTool === true;
235
249
  const enableLsp = session.enableLsp ?? true;
236
250
  const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
251
+ if (requestedTools && !requestedTools.includes("exit_plan_mode")) {
252
+ requestedTools.push("exit_plan_mode");
253
+ }
237
254
  const pythonMode = getPythonModeFromEnv() ?? session.settings?.getPythonToolMode?.() ?? "ipy-only";
238
255
  const skipPythonPreflight = session.skipPythonPreflight === true;
239
256
  let pythonAvailable = true;
@@ -296,6 +313,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
296
313
  : [
297
314
  ...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
298
315
  ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
316
+ ...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
299
317
  ];
300
318
  time("createTools:beforeFactories");
301
319
  const slowTools: Array<{ name: string; ms: number }> = [];
@@ -0,0 +1,46 @@
1
+ import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
2
+ import type { ToolSession } from ".";
3
+ import { resolveToCwd } from "./path-utils";
4
+ import { ToolError } from "./tool-errors";
5
+
6
+ const PLAN_URL_PREFIX = "plan://";
7
+
8
+ export function resolvePlanPath(session: ToolSession, targetPath: string): string {
9
+ if (!targetPath.startsWith(PLAN_URL_PREFIX)) {
10
+ return resolveToCwd(targetPath, session.cwd);
11
+ }
12
+
13
+ const settingsManager = session.settingsManager;
14
+ if (!settingsManager) {
15
+ throw new ToolError("Plan mode: settings manager unavailable for plan path resolution.");
16
+ }
17
+
18
+ return resolvePlanUrlToPath(targetPath, {
19
+ getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
20
+ cwd: session.cwd,
21
+ });
22
+ }
23
+
24
+ export function enforcePlanModeWrite(
25
+ session: ToolSession,
26
+ targetPath: string,
27
+ options?: { rename?: string; op?: "create" | "update" | "delete" },
28
+ ): void {
29
+ const state = session.getPlanModeState?.();
30
+ if (!state?.enabled) return;
31
+
32
+ const resolvedTarget = resolvePlanPath(session, targetPath);
33
+ const resolvedPlan = resolvePlanPath(session, state.planFilePath);
34
+
35
+ if (options?.rename) {
36
+ throw new ToolError("Plan mode: renaming files is not allowed.");
37
+ }
38
+
39
+ if (options?.op === "delete") {
40
+ throw new ToolError("Plan mode: deleting files is not allowed.");
41
+ }
42
+
43
+ if (resolvedTarget !== resolvedPlan) {
44
+ throw new ToolError(`Plan mode: only the plan file may be modified (${state.planFilePath}).`);
45
+ }
46
+ }
package/src/tools/read.ts CHANGED
@@ -162,10 +162,11 @@ function similarityScore(a: string, b: string): number {
162
162
  async function listCandidateFiles(
163
163
  searchRoot: string,
164
164
  signal?: AbortSignal,
165
+ notify?: (message: string) => void,
165
166
  ): Promise<{ files: string[]; truncated: boolean; error?: string }> {
166
167
  let fdPath: string | undefined;
167
168
  try {
168
- fdPath = await ensureTool("fd", true);
169
+ fdPath = await ensureTool("fd", { silent: true, notify });
169
170
  } catch {
170
171
  return { files: [], truncated: false, error: "fd not available" };
171
172
  }
@@ -248,6 +249,7 @@ async function findReadPathSuggestions(
248
249
  rawPath: string,
249
250
  cwd: string,
250
251
  signal?: AbortSignal,
252
+ notify?: (message: string) => void,
251
253
  ): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
252
254
  const resolvedPath = resolveToCwd(rawPath, cwd);
253
255
  const searchRoot = await findExistingDirectory(path.dirname(resolvedPath), signal);
@@ -262,7 +264,7 @@ async function findReadPathSuggestions(
262
264
  }
263
265
  }
264
266
 
265
- const { files, truncated, error } = await listCandidateFiles(searchRoot, signal);
267
+ const { files, truncated, error } = await listCandidateFiles(searchRoot, signal, notify);
266
268
  const scopeLabel = formatScopeLabel(searchRoot, cwd);
267
269
 
268
270
  if (error && files.length === 0) {
@@ -418,7 +420,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
418
420
  params: ReadParams,
419
421
  signal?: AbortSignal,
420
422
  _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
421
- _context?: AgentToolContext,
423
+ toolContext?: AgentToolContext,
422
424
  ): Promise<AgentToolResult<ReadToolDetails>> {
423
425
  const { path: readPath, offset, limit, lines } = params;
424
426
 
@@ -442,7 +444,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
442
444
 
443
445
  // Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
444
446
  if (!isRemoteMountPath(absolutePath)) {
445
- const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal);
447
+ const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal, message =>
448
+ toolContext?.ui?.notify(message, "info"),
449
+ );
446
450
 
447
451
  if (suggestions?.suggestions.length) {
448
452
  const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
@@ -17,7 +17,7 @@ import writeDescription from "../prompts/tools/write.md" with { type: "text" };
17
17
  import type { ToolSession } from "../sdk";
18
18
  import { renderStatusLine } from "../tui";
19
19
  import { type OutputMeta, outputMeta } from "./output-meta";
20
- import { resolveToCwd } from "./path-utils";
20
+ import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
21
21
  import {
22
22
  formatDiagnostics,
23
23
  formatExpandHint,
@@ -94,7 +94,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
94
94
  context?: AgentToolContext,
95
95
  ): Promise<AgentToolResult<WriteToolDetails>> {
96
96
  return untilAborted(signal, async () => {
97
- const absolutePath = resolveToCwd(path, this.session.cwd);
97
+ enforcePlanModeWrite(this.session, path, { op: "create" });
98
+ const absolutePath = resolvePlanPath(this.session, path);
98
99
  const batchRequest = getLspBatchRequest(context?.toolCall);
99
100
 
100
101
  const diagnostics = await this.writethrough(absolutePath, content, signal, undefined, batchRequest);
@@ -6,6 +6,7 @@ import { $ } from "bun";
6
6
  import { APP_NAME, getBinDir } from "../config";
7
7
 
8
8
  const TOOLS_DIR = getBinDir();
9
+ const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
9
10
 
10
11
  interface ToolConfig {
11
12
  name: string;
@@ -159,9 +160,18 @@ export async function getToolPath(tool: ToolName): Promise<string | null> {
159
160
 
160
161
  // Fetch latest release version from GitHub
161
162
  async function getLatestVersion(repo: string): Promise<string> {
162
- const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
163
- headers: { "User-Agent": `${APP_NAME}-coding-agent` },
164
- });
163
+ let response: Response;
164
+ try {
165
+ response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
166
+ headers: { "User-Agent": `${APP_NAME}-coding-agent` },
167
+ signal: AbortSignal.timeout(TOOL_DOWNLOAD_TIMEOUT_MS),
168
+ });
169
+ } catch (err) {
170
+ if (err instanceof Error && err.name === "AbortError") {
171
+ throw new Error("GitHub API request timed out");
172
+ }
173
+ throw err;
174
+ }
165
175
 
166
176
  if (!response.ok) {
167
177
  throw new Error(`GitHub API error: ${response.status}`);
@@ -173,7 +183,17 @@ async function getLatestVersion(repo: string): Promise<string> {
173
183
 
174
184
  // Download a file from URL
175
185
  async function downloadFile(url: string, dest: string): Promise<void> {
176
- const response = await fetch(url);
186
+ let response: Response;
187
+ try {
188
+ response = await fetch(url, {
189
+ signal: AbortSignal.timeout(TOOL_DOWNLOAD_TIMEOUT_MS),
190
+ });
191
+ } catch (err) {
192
+ if (err instanceof Error && err.name === "AbortError") {
193
+ throw new Error(`Download timed out: ${url}`);
194
+ }
195
+ throw err;
196
+ }
177
197
  if (!response.ok) {
178
198
  throw new Error(`Failed to download: ${response.status}`);
179
199
  } else if (!response.body) {
@@ -223,15 +243,12 @@ async function downloadTool(tool: ToolName): Promise<string> {
223
243
  const tmp = await TempDir.create("@omp-tools-extract-");
224
244
 
225
245
  try {
226
- if (assetName.endsWith(".tar.gz")) {
246
+ if (assetName.endsWith(".tar.gz") || assetName.endsWith(".zip")) {
227
247
  const archive = new Bun.Archive(await Bun.file(archivePath).arrayBuffer());
228
248
  const files = await archive.files();
229
249
  for (const [filePath, file] of files) {
230
250
  await Bun.write(path.join(tmp.path(), filePath), file);
231
251
  }
232
- } else if (assetName.endsWith(".zip")) {
233
- await fs.mkdir(tmp.path(), { recursive: true });
234
- await $`unzip -o ${archivePath} -d ${tmp.path()}`.quiet().nothrow();
235
252
  }
236
253
 
237
254
  // Find the binary in extracted files
@@ -284,7 +301,17 @@ async function installPythonPackage(pkg: string): Promise<boolean> {
284
301
 
285
302
  // Ensure a tool is available, downloading if necessary
286
303
  // Returns the path to the tool, or null if unavailable
287
- export async function ensureTool(tool: ToolName, silent: boolean = false): Promise<string | undefined> {
304
+ type EnsureToolOptions = {
305
+ silent?: boolean;
306
+ notify?: (message: string) => void;
307
+ };
308
+
309
+ export async function ensureTool(
310
+ tool: ToolName,
311
+ silentOrOptions: boolean | EnsureToolOptions = false,
312
+ ): Promise<string | undefined> {
313
+ const options = typeof silentOrOptions === "object" ? silentOrOptions : { silent: silentOrOptions };
314
+ const silent = options.silent ?? false;
288
315
  const existingPath = await getToolPath(tool);
289
316
  if (existingPath) {
290
317
  return existingPath;
@@ -296,6 +323,7 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
296
323
  if (!silent) {
297
324
  logger.debug(`${pythonConfig.name} not found. Installing via uv/pip...`);
298
325
  }
326
+ options.notify?.(`Installing ${pythonConfig.name}...`);
299
327
  const success = await installPythonPackage(pythonConfig.package);
300
328
  if (success) {
301
329
  // Re-check for the command after installation
@@ -320,6 +348,7 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
320
348
  if (!silent) {
321
349
  logger.debug(`${config.name} not found. Downloading...`);
322
350
  }
351
+ options.notify?.(`Downloading ${config.name}...`);
323
352
 
324
353
  try {
325
354
  const path = await downloadTool(tool);
@@ -169,7 +169,9 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
169
169
  model: "sonar-pro",
170
170
  messages,
171
171
  return_related_questions: true,
172
- search_context_size: "high",
172
+ web_search_options: {
173
+ search_context_size: "high",
174
+ },
173
175
  };
174
176
 
175
177
  if (params.search_recency_filter) {
@@ -163,7 +163,9 @@ export interface PerplexityRequest {
163
163
  search_recency_filter?: "day" | "week" | "month" | "year";
164
164
  return_images?: boolean;
165
165
  return_related_questions?: boolean;
166
- search_context_size?: "low" | "medium" | "high";
166
+ web_search_options?: {
167
+ search_context_size?: "low" | "medium" | "high";
168
+ };
167
169
  }
168
170
 
169
171
  export interface PerplexitySearchResult {