diff --git a/apps/buddy/package.json b/apps/buddy/package.json index 76bc7d26..eb62bee4 100644 --- a/apps/buddy/package.json +++ b/apps/buddy/package.json @@ -49,8 +49,8 @@ }, "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.77", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-coding-agent": "^0.86.1", + "@earendil-works/pi-ai": "^0.87.1", + "@earendil-works/pi-coding-agent": "^0.87.1", "@js-temporal/polyfill": "^0.5.1", "@modelcontextprotocol/client": "2.0.0", "@mozilla/readability": "^0.6.0", diff --git a/apps/buddy/shared/conversation/modelSelection.ts b/apps/buddy/shared/conversation/modelSelection.ts index ae83e484..b9c6b003 100644 --- a/apps/buddy/shared/conversation/modelSelection.ts +++ b/apps/buddy/shared/conversation/modelSelection.ts @@ -27,6 +27,9 @@ const OPENAI_FAST_MODE_MODEL_IDS = new Set([ 'gpt-5.6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', + 'gpt-6-astra', + 'gpt-6-luna', + 'gpt-6-sol', ]) export interface BuddyServiceTierOption { diff --git a/patches/@earendil-works__pi-ai@0.87.1.patch b/patches/@earendil-works__pi-ai@0.87.1.patch new file mode 100644 index 00000000..71d1a7a0 --- /dev/null +++ b/patches/@earendil-works__pi-ai@0.87.1.patch @@ -0,0 +1,35 @@ +diff --git a/dist/api/openai-completions.js b/dist/api/openai-completions.js +--- a/dist/api/openai-completions.js ++++ b/dist/api/openai-completions.js +@@ -1141,6 +1141,14 @@ export function convertMessages(model, context, compat, options) { + } + lastRole = msg.role; + } ++ if (params.some((message) => message.role === "assistant" && ++ typeof message.reasoning_content === "string")) { ++ for (const message of params) { ++ if (message.role === "assistant" && message.reasoning_content === undefined) { ++ message.reasoning_content = ""; ++ } ++ } ++ } + return params; + } + function convertTools(tools, compat) { +diff --git a/dist/api/openai-responses-shared.js b/dist/api/openai-responses-shared.js +index a2049d5a1f633d7a96aa3cf080ded6310df8f97f..cd1f08e0f8300d04cae27662d8815731cd0a7083 100644 +--- a/dist/api/openai-responses-shared.js ++++ b/dist/api/openai-responses-shared.js +@@ -355,7 +355,11 @@ export async function processResponsesStream(openaiStream, output, stream, model + } + if (item.type === "message") { + applyMessagePhaseStopReason(item); +- const block = { type: "text", text: "" }; ++ const block = { ++ type: "text", ++ text: "", ++ textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined), ++ }; + output.content.push(block); + const slot = { type: "text", block, contentIndex: output.content.length - 1 }; + outputSlots.set(outputIndex, slot); diff --git a/patches/@earendil-works__pi-coding-agent@0.87.1.patch b/patches/@earendil-works__pi-coding-agent@0.87.1.patch new file mode 100644 index 00000000..2c92708c --- /dev/null +++ b/patches/@earendil-works__pi-coding-agent@0.87.1.patch @@ -0,0 +1,774 @@ +diff --git a/dist/core/tools/find-execution.js b/dist/core/tools/find-execution.js +new file mode 100644 +index 0000000000000000000000000000000000000000..dd03780decc4b91299340ba7c6b6742421290949 +--- /dev/null ++++ b/dist/core/tools/find-execution.js +@@ -0,0 +1,67 @@ ++import path from "node:path"; ++import process from "node:process"; ++import { Buffer } from "node:buffer"; ++import { ensureTool } from "../../utils/tools-manager.js"; ++import { pathExists, resolveToCwd } from "./path-utils.js"; ++import { DEFAULT_MAX_BYTES, truncateHead } from "./truncate.js"; ++import { runSearchProcess, searchLimit } from "./search-process.js"; ++ ++export async function executeFind(cwd, input, signal, operations, relativize) { ++ signal?.throwIfAborted(); ++ const searchPath = resolveToCwd(input.path || ".", cwd); ++ const limit = searchLimit(input.limit, 1000, 10_000); ++ if (!(await (operations?.exists ?? pathExists)(searchPath))) throw new Error(`Path not found: ${searchPath}`); ++ const lines = []; ++ let count = 0; ++ let bytes = 0; ++ let budgetReached = false; ++ const collect = (raw) => { ++ count++; ++ if (count > limit) return false; ++ const line = relativize(raw, searchPath); ++ bytes += Buffer.byteLength(line) + 1; ++ if (bytes > DEFAULT_MAX_BYTES - 1024) { ++ budgetReached = true; ++ return false; ++ } ++ lines.push(line); ++ }; ++ if (operations?.glob) { ++ const results = await operations.glob(input.pattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit: limit + 1 }); ++ signal?.throwIfAborted(); ++ for (const result of results) if (collect(result) === false) break; ++ } else { ++ const executable = await ensureTool("fd"); ++ if (!executable) throw new Error("fd is not available and could not be downloaded"); ++ const args = ["--glob", "--color=never", "--hidden", "--print0", "--max-results", String(limit + 1)]; ++ let insideGitRepo = false; ++ for (let current = searchPath;;) { ++ signal?.throwIfAborted(); ++ if (await pathExists(path.join(current, ".git"))) { insideGitRepo = true; break; } ++ const parent = path.dirname(current); ++ if (parent === current) break; ++ current = parent; ++ } ++ if (!insideGitRepo) args.push("--no-require-git"); ++ let pattern = input.pattern; ++ if (pattern.includes("/")) { ++ args.push("--full-path"); ++ if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") pattern = `**/${pattern}`; ++ if (process.platform === "win32") pattern = pattern.replaceAll("/", String.raw`[/\\]`); ++ } ++ args.push("--", pattern, searchPath); ++ await runSearchProcess(executable, args, signal, 0, record => collect(new TextDecoder("utf-8", { fatal: true }).decode(record))); ++ } ++ const output = lines.join("\n"); ++ const details = {}; ++ const notices = []; ++ if (count > limit) { details.resultLimitReached = limit; notices.push(`${limit} results limit reached; narrow the path or pattern`); } ++ if (budgetReached) { ++ details.truncation = { ...truncateHead(output), truncated: true, truncatedBy: "bytes" }; ++ notices.push("Search output budget reached; narrow the path or pattern"); ++ } ++ return { ++ content: [{ type: "text", text: (output || (budgetReached ? "Search output omitted" : "No files found matching pattern")) + (notices.length ? `\n\n[${notices.join(". ")}]` : "") }], ++ details: Object.keys(details).length ? details : undefined, ++ }; ++} +diff --git a/dist/core/tools/find.js b/dist/core/tools/find.js +index a3bab76c952c2a03d6534aba80a72f7743a52b94..5a01803160a07a0a10939100876428ecaed4cebf 100644 +--- a/dist/core/tools/find.js ++++ b/dist/core/tools/find.js +@@ -1,12 +1,9 @@ +-import { createInterface } from "node:readline"; +-import { spawn } from "child_process"; + import path from "path"; + import { Type } from "typebox"; +-import { ensureTool } from "../../utils/tools-manager.js"; +-import { pathExists, resolveToCwd } from "./path-utils.js"; ++import { executeFind } from "./find-execution.js"; + import { findRenderers } from "./renderers/find.js"; + import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +-import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js"; ++import { DEFAULT_MAX_BYTES, formatSize } from "./truncate.js"; + /** Relativize a find result against the search root and normalize it to posix separators. */ + export function relativizeFindResultPath(resultPath, searchPath, pathModule = path) { + const hadTrailingSeparator = resultPath.endsWith(pathModule.sep) || (pathModule.sep === "\\" && resultPath.endsWith("/")); +@@ -26,11 +23,6 @@ export const findToolSystemPromptContribution = { + guidelines: [], + }; + const DEFAULT_LIMIT = 1000; +-const defaultFindOperations = { +- exists: pathExists, +- // This is a placeholder. Actual fd execution happens in execute() when no custom glob is provided. +- glob: () => [], +-}; + export function createFindToolDefinition(cwd, options) { + const customOps = options?.operations; + return { +@@ -39,208 +31,8 @@ export function createFindToolDefinition(cwd, options) { + description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, + promptSnippet: findToolSystemPromptContribution.snippet, + parameters: findSchema, +- async execute(_toolCallId, { pattern, path: searchDir, limit }, signal, _onUpdate, ctx) { +- return new Promise((resolve, reject) => { +- if (signal?.aborted) { +- reject(new Error("Operation aborted")); +- return; +- } +- let settled = false; +- let stopChild; +- const settle = (fn) => { +- if (settled) +- return; +- settled = true; +- signal?.removeEventListener("abort", onAbort); +- stopChild = undefined; +- fn(); +- }; +- const onAbort = () => { +- stopChild?.(); +- settle(() => reject(new Error("Operation aborted"))); +- }; +- signal?.addEventListener("abort", onAbort, { once: true }); +- (async () => { +- try { +- const searchPath = resolveToCwd(searchDir || ".", ctx?.cwd || cwd); +- const effectiveLimit = limit ?? DEFAULT_LIMIT; +- const ops = customOps ?? defaultFindOperations; +- // If custom operations provide glob(), use that instead of fd. +- if (customOps?.glob) { +- if (!(await ops.exists(searchPath))) { +- settle(() => reject(new Error(`Path not found: ${searchPath}`))); +- return; +- } +- if (signal?.aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- const results = await ops.glob(pattern, searchPath, { +- ignore: ["**/node_modules/**", "**/.git/**"], +- limit: effectiveLimit, +- }); +- if (signal?.aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- if (results.length === 0) { +- settle(() => resolve({ +- content: [{ type: "text", text: "No files found matching pattern" }], +- details: undefined, +- })); +- return; +- } +- // Relativize paths against the search root for stable output. +- const relativized = results.map((p) => relativizeFindResultPath(p, searchPath)); +- const resultLimitReached = relativized.length >= effectiveLimit; +- const rawOutput = relativized.join("\n"); +- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); +- let resultOutput = truncation.content; +- const details = {}; +- const notices = []; +- if (resultLimitReached) { +- notices.push(`${effectiveLimit} results limit reached`); +- details.resultLimitReached = effectiveLimit; +- } +- if (truncation.truncated) { +- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); +- details.truncation = truncation; +- } +- if (notices.length > 0) { +- resultOutput += `\n\n[${notices.join(". ")}]`; +- } +- settle(() => resolve({ +- content: [{ type: "text", text: resultOutput }], +- details: Object.keys(details).length > 0 ? details : undefined, +- })); +- return; +- } +- // Default implementation uses fd. +- const fdPath = await ensureTool("fd"); +- if (signal?.aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- if (!fdPath) { +- settle(() => reject(new Error("fd is not available and could not be downloaded"))); +- return; +- } +- const args = ["--glob", "--color=never", "--hidden"]; +- // fd normally ignores .gitignore outside git repos, so keep --no-require-git +- // there. Inside repos, use fd's default git-aware behavior so parent +- // .gitignore rules stop at nested repo boundaries: +- // https://github.com/earendil-works/pi/issues/5960 +- let insideGitRepo = false; +- for (let current = searchPath;;) { +- if (await pathExists(path.join(current, ".git"))) { +- insideGitRepo = true; +- break; +- } +- const parent = path.dirname(current); +- if (parent === current) +- break; +- current = parent; +- } +- if (!insideGitRepo) +- args.push("--no-require-git"); +- args.push("--max-results", String(effectiveLimit)); +- // fd --glob matches against the basename unless --full-path is set; in --full-path +- // mode it matches against the absolute candidate path, so a path-containing +- // pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything. +- let effectivePattern = pattern; +- if (pattern.includes("/")) { +- args.push("--full-path"); +- if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") { +- effectivePattern = `**/${pattern}`; +- } +- // fd matches full paths using native separators on Windows. +- if (process.platform === "win32") +- effectivePattern = effectivePattern.replaceAll("/", String.raw `[/\\]`); +- } +- args.push("--", effectivePattern, searchPath); +- const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] }); +- const rl = createInterface({ input: child.stdout }); +- let stderr = ""; +- const lines = []; +- stopChild = () => { +- if (!child.killed) { +- child.kill(); +- } +- }; +- const cleanup = () => { +- rl.close(); +- }; +- child.stderr?.on("data", (chunk) => { +- stderr += chunk.toString(); +- }); +- rl.on("line", (line) => { +- lines.push(line); +- }); +- child.on("error", (error) => { +- cleanup(); +- settle(() => reject(new Error(`Failed to run fd: ${error.message}`))); +- }); +- child.on("close", (code) => { +- cleanup(); +- if (signal?.aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- const output = lines.join("\n"); +- if (code !== 0) { +- const errorMsg = stderr.trim() || `fd exited with code ${code}`; +- if (!output) { +- settle(() => reject(new Error(errorMsg))); +- return; +- } +- } +- if (!output) { +- settle(() => resolve({ +- content: [{ type: "text", text: "No files found matching pattern" }], +- details: undefined, +- })); +- return; +- } +- const relativized = []; +- for (const rawLine of lines) { +- const line = rawLine.replace(/\r$/, "").trim(); +- if (!line) +- continue; +- relativized.push(relativizeFindResultPath(line, searchPath)); +- } +- const resultLimitReached = relativized.length >= effectiveLimit; +- const rawOutput = relativized.join("\n"); +- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); +- let resultOutput = truncation.content; +- const details = {}; +- const notices = []; +- if (resultLimitReached) { +- notices.push(`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`); +- details.resultLimitReached = effectiveLimit; +- } +- if (truncation.truncated) { +- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); +- details.truncation = truncation; +- } +- if (notices.length > 0) { +- resultOutput += `\n\n[${notices.join(". ")}]`; +- } +- settle(() => resolve({ +- content: [{ type: "text", text: resultOutput }], +- details: Object.keys(details).length > 0 ? details : undefined, +- })); +- }); +- } +- catch (e) { +- if (signal?.aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- const error = e instanceof Error ? e : new Error(String(e)); +- settle(() => reject(error)); +- } +- })(); +- }); ++ async execute(_toolCallId, input, signal, _onUpdate, ctx) { ++ return executeFind(ctx?.cwd || cwd, input, signal, customOps, relativizeFindResultPath); + }, + ...findRenderers, + }; +diff --git a/dist/core/tools/grep-execution.js b/dist/core/tools/grep-execution.js +new file mode 100644 +index 0000000000000000000000000000000000000000..b72872c74e6c615946bc1a0ae0f791c85d160475 +--- /dev/null ++++ b/dist/core/tools/grep-execution.js +@@ -0,0 +1,82 @@ ++import path from "node:path"; ++import { Buffer } from "node:buffer"; ++import { ensureTool } from "../../utils/tools-manager.js"; ++import { resolveToCwd } from "./path-utils.js"; ++import { DEFAULT_MAX_BYTES, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine } from "./truncate.js"; ++import { runSearchProcess, searchLimit } from "./search-process.js"; ++ ++function eventText(value) { ++ if (typeof value?.text === "string") return value.text; ++ if (typeof value?.bytes === "string") { ++ const bytes = Buffer.from(value.bytes, "base64"); ++ return new TextDecoder("utf-8", { fatal: true }).decode(bytes); ++ } ++ throw new Error("Invalid ripgrep text record"); ++} ++ ++export async function executeGrep(cwd, input, signal, isDirectory) { ++ signal?.throwIfAborted(); ++ const executable = await ensureTool("rg"); ++ if (!executable) throw new Error("ripgrep (rg) is not available and could not be downloaded"); ++ const searchPath = resolveToCwd(input.path || ".", cwd); ++ const directory = await isDirectory(searchPath); ++ const limit = searchLimit(input.limit, 100, 10_000); ++ const context = input.context === undefined || input.context === 0 ? 0 : searchLimit(input.context, 0, 100); ++ const args = ["--json", "--no-config", "--line-number", "--color=never", "--hidden"]; ++ if (input.ignoreCase) args.push("--ignore-case"); ++ if (input.literal) args.push("--fixed-strings"); ++ if (input.glob) args.push("--glob", input.glob); ++ if (context) args.push("--context", String(context)); ++ args.push("--", input.pattern, searchPath); ++ const lines = []; ++ let bytes = 0; ++ let matches = 0; ++ let remainingContext = context; ++ let clipped = false; ++ let budgetReached = false; ++ await runSearchProcess(executable, args, signal, 10, (record) => { ++ const event = JSON.parse(record.toString("utf8")); ++ if (event.type === "end" && matches >= limit) return false; ++ if (event.type !== "match" && event.type !== "context") return; ++ const isMatch = event.type === "match"; ++ if (isMatch && matches >= limit) return false; ++ const file = eventText(event.data?.path); ++ const number = event.data?.line_number; ++ if (!Number.isSafeInteger(number) || number < 1) throw new Error("Invalid ripgrep line number"); ++ const shown = directory ? path.relative(searchPath, file).split(path.sep).join("/") : path.basename(file); ++ const value = eventText(event.data?.lines).replace(/\r?\n$/, ""); ++ const { text, wasTruncated } = truncateLine(value); ++ clipped ||= wasTruncated; ++ const line = isMatch ? `${shown}:${number}: ${text}` : `${shown}-${number}- ${text}`; ++ bytes += Buffer.byteLength(line) + 1; ++ if (bytes > DEFAULT_MAX_BYTES - 1024) { ++ budgetReached = true; ++ return false; ++ } ++ lines.push(line); ++ if (isMatch) matches++; ++ if (matches >= limit) { ++ if (!context) return false; ++ if (!isMatch && --remainingContext <= 0) return false; ++ } ++ }, [0, 1]); ++ const details = {}; ++ const notices = []; ++ if (matches >= limit) { ++ details.matchLimitReached = limit; ++ notices.push(`${limit} matches limit reached; narrow the path or pattern`); ++ } ++ if (clipped) { ++ details.linesTruncated = true; ++ notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars`); ++ } ++ const output = lines.join("\n"); ++ if (budgetReached) { ++ details.truncation = { ...truncateHead(output), truncated: true, truncatedBy: "bytes" }; ++ notices.push("Search output budget reached; narrow the path or pattern"); ++ } ++ return { ++ content: [{ type: "text", text: (output || (budgetReached ? "Search output omitted" : "No matches found")) + (notices.length ? `\n\n[${notices.join(". ")}]` : "") }], ++ details: Object.keys(details).length ? details : undefined, ++ }; ++} +diff --git a/dist/core/tools/grep.js b/dist/core/tools/grep.js +index d427d071582ead6e0c876121a8b67ad9fe6c45f7..082d3dde3f0985bf968013121cc5db9f65a2ad4f 100644 +--- a/dist/core/tools/grep.js ++++ b/dist/core/tools/grep.js +@@ -1,13 +1,9 @@ + import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises"; +-import { createInterface } from "node:readline"; +-import { spawn } from "child_process"; +-import path from "path"; + import { Type } from "typebox"; +-import { ensureTool } from "../../utils/tools-manager.js"; +-import { resolveToCwd } from "./path-utils.js"; ++import { executeGrep } from "./grep-execution.js"; + import { grepRenderers } from "./renderers/grep.js"; + import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +-import { DEFAULT_MAX_BYTES, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine, } from "./truncate.js"; ++import { DEFAULT_MAX_BYTES, GREP_MAX_LINE_LENGTH, } from "./truncate.js"; + const grepSchema = Type.Object({ + pattern: Type.String({ description: "Search pattern (regex or literal string)" }), + path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })), +@@ -34,214 +30,8 @@ export function createGrepToolDefinition(cwd, options) { + description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`, + promptSnippet: grepToolSystemPromptContribution.snippet, + parameters: grepSchema, +- async execute(_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit, }, signal, _onUpdate, ctx) { +- return new Promise((resolve, reject) => { +- if (signal?.aborted) { +- reject(new Error("Operation aborted")); +- return; +- } +- let settled = false; +- const settle = (fn) => { +- if (!settled) { +- settled = true; +- fn(); +- } +- }; +- (async () => { +- try { +- const rgPath = await ensureTool("rg"); +- if (!rgPath) { +- settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded"))); +- return; +- } +- const searchPath = resolveToCwd(searchDir || ".", ctx?.cwd || cwd); +- const ops = customOps ?? defaultGrepOperations; +- let isDirectory; +- try { +- isDirectory = await ops.isDirectory(searchPath); +- } +- catch { +- settle(() => reject(new Error(`Path not found: ${searchPath}`))); +- return; +- } +- const contextValue = context && context > 0 ? context : 0; +- const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT); +- const formatPath = (filePath) => { +- if (isDirectory) { +- const relative = path.relative(searchPath, filePath); +- if (relative && !relative.startsWith("..")) { +- return relative.replace(/\\/g, "/"); +- } +- } +- return path.basename(filePath); +- }; +- const fileCache = new Map(); +- const getFileLines = async (filePath) => { +- let lines = fileCache.get(filePath); +- if (!lines) { +- try { +- const content = await ops.readFile(filePath); +- lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); +- } +- catch { +- lines = []; +- } +- fileCache.set(filePath, lines); +- } +- return lines; +- }; +- const args = ["--json", "--line-number", "--color=never", "--hidden"]; +- if (ignoreCase) +- args.push("--ignore-case"); +- if (literal) +- args.push("--fixed-strings"); +- if (glob) +- args.push("--glob", glob); +- args.push("--", pattern, searchPath); +- const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] }); +- const rl = createInterface({ input: child.stdout }); +- let stderr = ""; +- let matchCount = 0; +- let matchLimitReached = false; +- let linesTruncated = false; +- let aborted = false; +- let killedDueToLimit = false; +- const outputLines = []; +- const cleanup = () => { +- rl.close(); +- signal?.removeEventListener("abort", onAbort); +- }; +- const stopChild = (dueToLimit = false) => { +- if (!child.killed) { +- killedDueToLimit = dueToLimit; +- child.kill(); +- } +- }; +- const onAbort = () => { +- aborted = true; +- stopChild(); +- }; +- signal?.addEventListener("abort", onAbort, { once: true }); +- child.stderr?.on("data", (chunk) => { +- stderr += chunk.toString(); +- }); +- const formatBlock = async (filePath, lineNumber) => { +- const relativePath = formatPath(filePath); +- const lines = await getFileLines(filePath); +- if (!lines.length) +- return [`${relativePath}:${lineNumber}: (unable to read file)`]; +- const block = []; +- const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber; +- const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber; +- for (let current = start; current <= end; current++) { +- const lineText = lines[current - 1] ?? ""; +- const sanitized = lineText.replace(/\r/g, ""); +- const isMatchLine = current === lineNumber; +- // Truncate long lines so grep output stays compact. +- const { text: truncatedText, wasTruncated } = truncateLine(sanitized); +- if (wasTruncated) +- linesTruncated = true; +- if (isMatchLine) +- block.push(`${relativePath}:${current}: ${truncatedText}`); +- else +- block.push(`${relativePath}-${current}- ${truncatedText}`); +- } +- return block; +- }; +- // Collect matches during streaming, then format them after rg exits. +- const matches = []; +- rl.on("line", (line) => { +- if (!line.trim() || matchCount >= effectiveLimit) +- return; +- let event; +- try { +- event = JSON.parse(line); +- } +- catch { +- return; +- } +- if (event.type === "match") { +- matchCount++; +- const filePath = event.data?.path?.text; +- const lineNumber = event.data?.line_number; +- const lineText = event.data?.lines?.text; +- if (filePath && typeof lineNumber === "number") +- matches.push({ filePath, lineNumber, lineText }); +- if (matchCount >= effectiveLimit) { +- matchLimitReached = true; +- stopChild(true); +- } +- } +- }); +- child.on("error", (error) => { +- cleanup(); +- settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`))); +- }); +- child.on("close", async (code) => { +- cleanup(); +- if (aborted) { +- settle(() => reject(new Error("Operation aborted"))); +- return; +- } +- if (!killedDueToLimit && code !== 0 && code !== 1) { +- const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`; +- settle(() => reject(new Error(errorMsg))); +- return; +- } +- if (matchCount === 0) { +- settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined })); +- return; +- } +- // Format matches after streaming finishes so custom readFile() backends can be async. +- for (const match of matches) { +- if (contextValue === 0 && match.lineText !== undefined) { +- const relativePath = formatPath(match.filePath); +- const sanitized = match.lineText +- .replace(/\r\n/g, "\n") +- .replace(/\r/g, "") +- .replace(/\n$/, ""); +- const { text: truncatedText, wasTruncated } = truncateLine(sanitized); +- if (wasTruncated) +- linesTruncated = true; +- outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`); +- } +- else { +- const block = await formatBlock(match.filePath, match.lineNumber); +- outputLines.push(...block); +- } +- } +- const rawOutput = outputLines.join("\n"); +- // Apply byte truncation. There is no line limit here because the match limit already capped rows. +- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); +- let output = truncation.content; +- const details = {}; +- // Build actionable notices for truncation and match limits. +- const notices = []; +- if (matchLimitReached) { +- notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`); +- details.matchLimitReached = effectiveLimit; +- } +- if (truncation.truncated) { +- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); +- details.truncation = truncation; +- } +- if (linesTruncated) { +- notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`); +- details.linesTruncated = true; +- } +- if (notices.length > 0) +- output += `\n\n[${notices.join(". ")}]`; +- settle(() => resolve({ +- content: [{ type: "text", text: output }], +- details: Object.keys(details).length > 0 ? details : undefined, +- })); +- }); +- } +- catch (err) { +- settle(() => reject(err)); +- } +- })(); +- }); ++ async execute(_toolCallId, input, signal, _onUpdate, ctx) { ++ return executeGrep(ctx?.cwd || cwd, input, signal, (customOps ?? defaultGrepOperations).isDirectory); + }, + ...grepRenderers, + }; +diff --git a/dist/core/tools/search-process.js b/dist/core/tools/search-process.js +new file mode 100644 +index 0000000000000000000000000000000000000000..a8979704a0a1b9bf1052f3dd4396afb851743d3e +--- /dev/null ++++ b/dist/core/tools/search-process.js +@@ -0,0 +1,74 @@ ++import { spawn } from "node:child_process"; ++import { Buffer } from "node:buffer"; ++ ++const MAX_CAPTURE_BYTES = 8 * 1024 * 1024; ++const MAX_STDERR_BYTES = 16 * 1024; ++ ++export function runSearchProcess(executable, args, signal, separator, onRecord, successCodes = [0]) { ++ return new Promise((resolve, reject) => { ++ signal?.throwIfAborted(); ++ const child = spawn(executable, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); ++ let failure; ++ let stopped = false; ++ let total = 0; ++ let stderr = Buffer.alloc(0); ++ let pending = []; ++ const stop = (error) => { ++ failure ??= error; ++ stopped = true; ++ child.kill("SIGKILL"); ++ }; ++ const abort = () => stop(new Error("Operation aborted", { cause: signal?.reason })); ++ const timer = setTimeout(() => stop(new Error("Search timed out; narrow the search path")), 30_000); ++ signal?.addEventListener("abort", abort, { once: true }); ++ if (signal?.aborted) abort(); ++ const emit = (tail) => { ++ const record = pending.length ? Buffer.concat([...pending, tail]) : tail; ++ pending = []; ++ if (record.length && onRecord(record) === false) stop(); ++ }; ++ child.stdout.on("data", (chunk) => { ++ if (stopped) return; ++ total += chunk.length; ++ if (total > MAX_CAPTURE_BYTES) { ++ stop(new Error("Search output limit exceeded; narrow the search path or pattern")); ++ return; ++ } ++ try { ++ let start = 0; ++ for (let end = chunk.indexOf(separator); end !== -1; end = chunk.indexOf(separator, start)) { ++ emit(chunk.subarray(start, end)); ++ if (stopped) return; ++ start = end + 1; ++ } ++ if (start < chunk.length) pending.push(chunk.subarray(start)); ++ } catch (error) { stop(error); } ++ }); ++ child.stderr.on("data", (chunk) => { ++ if (stopped) return; ++ if (stderr.length + chunk.length > MAX_STDERR_BYTES) { ++ stop(new Error("Search diagnostic output limit exceeded")); ++ return; ++ } ++ stderr = Buffer.concat([stderr, chunk]); ++ }); ++ child.once("error", (error) => { failure ??= error; }); ++ child.once("close", (code) => { ++ clearTimeout(timer); ++ signal?.removeEventListener("abort", abort); ++ if (!failure && !stopped && !successCodes.includes(code)) ++ failure = new Error(stderr.toString("utf8").trim() || `Search exited with code ${code}`); ++ if (!failure && !stopped && pending.length) { ++ try { emit(Buffer.alloc(0)); } catch (error) { failure = error; } ++ } ++ if (failure) reject(failure); ++ else resolve(); ++ }); ++ }); ++} ++ ++export function searchLimit(value, fallback, maximum) { ++ if (value === undefined) return fallback; ++ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Search limits must be non-negative integers"); ++ return Math.min(Math.max(1, value), maximum); ++} +diff --git a/dist/utils/shell.js b/dist/utils/shell.js +index 19605cc7dbd8b6752777e47c5c310c613b35c505..5777711e4ef72900066da6115b183fbac28c09b6 100644 +--- a/dist/utils/shell.js ++++ b/dist/utils/shell.js +@@ -1,5 +1,5 @@ + import { existsSync } from "node:fs"; +-import { delimiter, join } from "node:path"; ++import { delimiter, isAbsolute, join } from "node:path"; + import { spawn, spawnSync } from "child_process"; + import { getBinDir } from "../config.js"; + /** +@@ -106,6 +106,12 @@ export function getPowerShellConfig() { + if (process.platform !== "win32") { + throw new Error("The powershell tool is only available on Windows."); + } ++ const configuredShell = process.env.PI_POWERSHELL_PATH; ++ if (configuredShell !== undefined) { ++ if (!isAbsolute(configuredShell) || !existsSync(configuredShell)) ++ throw new Error("PI_POWERSHELL_PATH must point to an absolute PowerShell executable"); ++ return { shell: configuredShell, args: [...POWERSHELL_ARGS] }; ++ } + const shell = findExecutableOnPath("pwsh.exe") ?? findExecutableOnPath("powershell.exe"); + if (!shell) { + throw new Error("No PowerShell executable found. Install PowerShell or add powershell.exe/pwsh.exe to PATH."); +diff --git a/dist/utils/tools-manager.js b/dist/utils/tools-manager.js +index 39b1f1ae3357925c68127b580340208c5d557ed9..f2747a351c5dfb83b9b25d93dffb78681e1f9022 100644 +--- a/dist/utils/tools-manager.js ++++ b/dist/utils/tools-manager.js +@@ -1,7 +1,7 @@ + import { spawnSync } from "child_process"; + import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs"; + import { arch, platform } from "os"; +-import { join } from "path"; ++import { isAbsolute, join } from "path"; + import { Readable } from "stream"; + import { pipeline } from "stream/promises"; + import { APP_NAME, getBinDir } from "../config.js"; +@@ -76,6 +76,16 @@ export function getToolPath(tool) { + const config = TOOLS[tool]; + if (!config) + return null; ++ const bundledDirectory = process.env.PI_TOOLS_DIR; ++ if (bundledDirectory !== undefined) { ++ if (!isAbsolute(bundledDirectory)) ++ throw new Error("PI_TOOLS_DIR must be an absolute directory"); ++ const bundledPath = join(bundledDirectory, config.binaryName + (platform() === "win32" ? ".exe" : "")); ++ if (!existsSync(bundledPath)) ++ throw new Error(`Bundled tool is missing: ${bundledPath}`); ++ return bundledPath; ++ } ++ + // Check our tools directory first + const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : "")); + if (existsSync(localPath)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed9c8731..d9232e8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,8 +197,8 @@ overrides: patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.77': b859621ca2a537d1865ef159feeccc66fb80b1470d4aef8748318d6cb84fdc50 - '@earendil-works/pi-ai@0.86.1': 5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb - '@earendil-works/pi-coding-agent@0.86.1': 3a6d09cf229dcb59d4200db8c6f7aecb4bb198c563b0381935fac35b8cfa9794 + '@earendil-works/pi-ai@0.87.1': 5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb + '@earendil-works/pi-coding-agent@0.87.1': aeb1d0976c9bee06023ddc36c53f8c95e67bb7572f94c11d1e37ab24e015b653 importers: @@ -411,11 +411,11 @@ importers: specifier: 0.0.77 version: 0.0.77(patch_hash=b859621ca2a537d1865ef159feeccc66fb80b1470d4aef8748318d6cb84fdc50) '@earendil-works/pi-ai': - specifier: ^0.86.1 - version: 0.86.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + specifier: ^0.87.1 + version: 0.87.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-coding-agent': - specifier: ^0.86.1 - version: 0.86.1(patch_hash=3a6d09cf229dcb59d4200db8c6f7aecb4bb198c563b0381935fac35b8cfa9794)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + specifier: ^0.87.1 + version: 0.87.1(patch_hash=aeb1d0976c9bee06023ddc36c53f8c95e67bb7572f94c11d1e37ab24e015b653)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) '@js-temporal/polyfill': specifier: ^0.5.1 version: 0.5.1 @@ -1431,34 +1431,34 @@ packages: oxlint: optional: true - '@earendil-works/chord@0.86.1': - resolution: {integrity: sha512-GzUr5n4tFBHUYxN9CjcRHK8QWo9tbxNrZu6iWPQ+PFiFrLASvSZOKeAVAgh3gHv/t0X5OvUpFlrMQ/nEFfCYpg==} + '@earendil-works/chord@0.87.1': + resolution: {integrity: sha512-bg7IkJGFcEaMqqYgOGUiq5Ky9RghpRfrlZ8I/v/1b4bBZ02A7t3E+6uhPRbadwWb/kWsnVFbZsqOKRN4a3LLCg==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-agent-core@0.86.1': - resolution: {integrity: sha512-8TbBzhYsDeu5V1Zl2NsyrBqJAzX1EiEL3Np3ZjGpy0pSDdGRVOpcyW1qruLqfWmEqGcnxmvgnTMLS/wJNZO2XQ==} + '@earendil-works/pi-agent-core@0.87.1': + resolution: {integrity: sha512-Zev3B0HK7YS5A4EZQ2XnEqiJuirx6QBiltJ+LpmjV5a/+2IU0cfKtIfnkNkORK707XOvKBY2WRtk7cAwHpbh2Q==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.86.1': - resolution: {integrity: sha512-1XHhI6D/fyQdsBieHC/E/4zGKVOoGe4yDyX67VXvzoYkFsX/qE7NpZE7E1RC8e6Bz8B9oG/P+MQFXikv2/BGEg==} + '@earendil-works/pi-ai@0.87.1': + resolution: {integrity: sha512-X/3PfQBnnoeVdO9Cv8zHghUMglzlgNZYGNzoPnbRoGnHl3Rw3TlA2UKSUB7BRHUOxMryHXYa8dnjWZlbRheDZA==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.86.1': - resolution: {integrity: sha512-vZBuNfJnruxZyemZ3O05V0S/Ylze08ahFTIQ1Mik++gVdOevPl89gt/Uv0U97BPAJaj9cj6Vf9rcIgKtUrd0BA==} + '@earendil-works/pi-coding-agent@0.87.1': + resolution: {integrity: sha512-m8ArJUtVcQMSe1lLE/Ei7vX/JV7O39sWmWBsXV2NOU70F0qCp8GubA24pT3LnwTmM6LL2xV80/h6sQg85n69ew==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-telemetry@0.86.1': - resolution: {integrity: sha512-SOcEqOS3oVGgKeahs2jHB906d8hFjuLP+RBee8xKYMRgw5KAeWHNg+YABfL0ALlp3Bt6tW4b632MLghc3vnTog==} + '@earendil-works/pi-telemetry@0.87.1': + resolution: {integrity: sha512-MC6TRQH5lgMXpcN+Vku2WMI2T8BsiUPzMQHGo81uqFZD3/9O79WWJAysEDGuzduP6R4tvtgwMLwmqIxynM10JQ==} engines: {node: '>=22.19.0'} '@earendil-works/pi-tui@0.78.0': resolution: {integrity: sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-tui@0.86.1': - resolution: {integrity: sha512-FU/zU/zG4RWokcZt+BVXXcieWi5ggvYnWP2kkB5XXjMaHRoy5BDhcZJ9JAnLTN9MwrCRoXgPQxOI0bFqwYeZkQ==} + '@earendil-works/pi-tui@0.87.1': + resolution: {integrity: sha512-YEH2vRyOeiO7hhN6j6AE6YwKSq2Kz2f3XR8bj1TbR+aGE/JsnY1hLPMI2pvaZfRM1n9Y00tejxFQ4zbzvF7nkQ==} engines: {node: '>=22.19.0'} '@electric-sql/pglite-socket@0.1.3': @@ -9996,15 +9996,15 @@ snapshots: optionalDependencies: eslint: 10.4.0(jiti@2.7.0)(supports-color@8.1.1) - '@earendil-works/chord@0.86.1': + '@earendil-works/chord@0.87.1': dependencies: esbuild: 0.28.2 - '@earendil-works/pi-agent-core@0.86.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.87.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/chord': 0.86.1 - '@earendil-works/pi-ai': 0.86.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-telemetry': 0.86.1 + '@earendil-works/chord': 0.87.1 + '@earendil-works/pi-ai': 0.87.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-telemetry': 0.87.1 diff: 8.0.4 ignore: 7.0.8 typebox: 1.3.27 @@ -10018,11 +10018,11 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.86.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.87.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.124.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 - '@earendil-works/pi-telemetry': 0.86.1 + '@earendil-works/pi-telemetry': 0.87.1 '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@8.1.1) @@ -10039,12 +10039,12 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.86.1(patch_hash=3a6d09cf229dcb59d4200db8c6f7aecb4bb198c563b0381935fac35b8cfa9794)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.87.1(patch_hash=aeb1d0976c9bee06023ddc36c53f8c95e67bb7572f94c11d1e37ab24e015b653)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/chord': 0.86.1 - '@earendil-works/pi-agent-core': 0.86.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.86.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-tui': 0.86.1 + '@earendil-works/chord': 0.87.1 + '@earendil-works/pi-agent-core': 0.87.1(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.87.1(patch_hash=5a203271dc6590ed6f6b52a3974fc7bf58ec5e17c7e19a543ec5474ae9c516eb)(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@8.1.1)(zod@4.4.3))(supports-color@8.1.1)(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-tui': 0.87.1 '@silvia-odwyer/photon-node': 0.3.4 chalk: 6.0.0 cross-spawn: 7.0.6 @@ -10069,14 +10069,14 @@ snapshots: - ws - zod - '@earendil-works/pi-telemetry@0.86.1': {} + '@earendil-works/pi-telemetry@0.87.1': {} '@earendil-works/pi-tui@0.78.0': dependencies: get-east-asian-width: 1.6.0 marked: 15.0.12 - '@earendil-works/pi-tui@0.86.1': + '@earendil-works/pi-tui@0.87.1': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.11 @@ -11652,8 +11652,8 @@ snapshots: '@smithy/signature-v4@5.7.0': dependencies: - '@smithy/core': 3.32.0 - '@smithy/types': 4.17.0 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 '@smithy/types@4.14.2': @@ -14509,7 +14509,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8a2adaaf..09425805 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,8 +40,8 @@ allowBuilds: vue-demi: true patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.77': patches/@anthropic-ai__sandbox-runtime@0.0.77.patch - '@earendil-works/pi-ai@0.86.1': patches/@earendil-works__pi-ai@0.86.1.patch - '@earendil-works/pi-coding-agent@0.86.1': patches/@earendil-works__pi-coding-agent@0.86.1.patch + '@earendil-works/pi-ai@0.87.1': patches/@earendil-works__pi-ai@0.87.1.patch + '@earendil-works/pi-coding-agent@0.87.1': patches/@earendil-works__pi-coding-agent@0.87.1.patch catalog: '@types/node': ^25.6.0 dotenv: ^17.4.2 @@ -52,9 +52,9 @@ catalog: typescript: ^6.0.3 zod: ^4.3.6 minimumReleaseAgeExclude: - - '@earendil-works/chord@0.86.1' - - '@earendil-works/pi-agent-core@0.86.1' - - '@earendil-works/pi-ai@0.86.1' - - '@earendil-works/pi-coding-agent@0.86.1' - - '@earendil-works/pi-telemetry@0.86.1' - - '@earendil-works/pi-tui@0.86.1' + - '@earendil-works/chord@0.87.1' + - '@earendil-works/pi-agent-core@0.87.1' + - '@earendil-works/pi-ai@0.87.1' + - '@earendil-works/pi-coding-agent@0.87.1' + - '@earendil-works/pi-telemetry@0.87.1' + - '@earendil-works/pi-tui@0.87.1'