From 50586359edb25044baa18861699593c0f91dd091 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 09:21:37 +0200 Subject: [PATCH 01/17] feat(agent): provider-agnostic expert models + self-directed tool loop (#7, #11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11: vision-expert subagents (task/task_batch) now run a bounded agentic loop instead of a single completion. They can call `view_region` (zoom into a normalized bbox at full resolution) and `view_page` (load another page from the source) to pull in detail on demand, rather than depending on the orchestrator to guess the right crop up front. The loop is capped at 8 tool calls; the full exchange (tool calls + results) is kept in the session and persisted as `steps`, with tool-result images re-cropped from disk on restore (no base64 bloat). Tools are only offered to models that can consume images, and the loop honors the caller's abort signal (threaded through task/task_batch). #7: removes the hardcoded `google/gemini-3-flash-preview` default. When no model is specified, experts fall back to the orchestrator's current model (whatever the user has selected/authed in pi) — no provider is baked in. Model recommendations move to the docs. Prompts/tool descriptions are de-Geminified and document the new self-zoom tools. Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos/prompts/page-expert-prompt.md | 10 ++ chronos/prompts/system-prompt.md | 18 ++- chronos/prompts/task-batch.md | 4 +- chronos/prompts/task.md | 2 +- chronos/tools/expert-tools.ts | 165 ++++++++++++++++++++++++++ chronos/tools/expert-turn.ts | 144 +++++++++++++++++----- chronos/tools/task-batch.ts | 11 +- chronos/tools/view-page.ts | 14 +-- chronos/utils/expert-store.ts | 19 ++- chronos/utils/resolve-model.ts | 28 ++++- 10 files changed, 360 insertions(+), 55 deletions(-) create mode 100644 chronos/tools/expert-tools.ts diff --git a/chronos/prompts/page-expert-prompt.md b/chronos/prompts/page-expert-prompt.md index 816903d..2ef4efd 100644 --- a/chronos/prompts/page-expert-prompt.md +++ b/chronos/prompts/page-expert-prompt.md @@ -1,6 +1,16 @@ You are an expert analyst of historical German documents (18th-20th century). You are given a scanned page image. Answer the user's question about the page accurately and concisely. +## Tools +You can request more imagery before answering — use them when the full-page view is not enough: +- **`view_region(bbox, [page_id])`** — zoom into a sub-region at full resolution to read dense tables, + marginalia, or faint/damaged ink. `bbox` is normalized 0–1 (`x`/`y` = top-left, `w`/`h` = size). + Omit `page_id` to zoom into the page you are currently looking at. +- **`view_page(page_id)`** — load another full page from the same source (e.g. to follow a record that + continues overleaf). `page_id` is the file-system index (1 = page_0001.png), not the printed number. + +Prefer zooming in over guessing. When you have enough detail, stop calling tools and give your answer. + ## General rules - If an internal page number is visible on the page, state it in the first sentence, quoted exactly as printed (including Roman numerals if applicable). If no page number is visible, do not speculate. - Be factual and do not speculate beyond what is visible on the page. diff --git a/chronos/prompts/system-prompt.md b/chronos/prompts/system-prompt.md index e2bb8e9..e09dad1 100644 --- a/chronos/prompts/system-prompt.md +++ b/chronos/prompts/system-prompt.md @@ -57,6 +57,15 @@ in the Data tab (with a "show full page" option): Example row: `{ "surname": "Müller", "trade": "baker", "chronos_page": 42, "chronos_bbox": [0.1, 0.32, 0.8, 0.05] }` +**Multiple references per row.** When a single row draws on more than one place — a value +split across two pages, or assembled from several regions of one page — make any of these keys +a **list**; they align by index and the Data tab renders one citation per reference. A scalar +is treated as a single reference, and a length-1 list broadcasts (one `chronos_source` shared +across pages, or several `chronos_bbox` regions on one `chronos_page`). Every reference must +have a page. +- Two pages: `{ "name": "Anna Weber", "chronos_page": [42, 43], "chronos_bbox": [[0.1, 0.9, 0.8, 0.06], [0.1, 0.04, 0.8, 0.06]] }` +- Two regions of one page: `{ "name": "Karl Vogt", "chronos_page": 42, "chronos_bbox": [[0.1, 0.32, 0.8, 0.05], [0.55, 0.32, 0.4, 0.05]] }` + These keys are a recommendation, not a requirement — free-form text/CSV files still show in the Data tab (as text). Always write outputs under `{{sourceDataDir}}/`. @@ -77,11 +86,12 @@ the Data tab (as text). Always write outputs under `{{sourceDataDir}}/`. model in a persistent conversation. Without `task_id` it spawns a new expert and the result ends with a `task_id:` line; pass that id back to ask the same expert follow-up questions ("What did that abbreviation mean?") without re-sending earlier images. Multiple experts can - be active concurrently, each with its own history. `page_id` optionally attaches a page image + be active concurrently, each with its own history. The expert can self-direct with `view_region` + (zoom into a sub-region) and `view_page` (load another page from the source), so you needn't + predict the exact crop up front. `page_id` optionally attaches a page image (on spawn or follow-up — requires an active source); omit it for text-only messages. - `model` accepts any configured model as `provider/model-id` — default - `google/gemini-3-flash-preview`; pick a stronger model (e.g. `google/gemini-3.1-pro-preview`) - for difficult pages. + `model` accepts any model pi has auth for as `provider/model-id`; it defaults to the + orchestrator's current model. Pass `model` to use a different one for a hard page. - **`task_batch(page_ids, prompt, [model], [output_file], [concurrency], [bbox])`** — the batch version of `task`: spawns one expert per page in parallel. Each page becomes its own persistent session with its own `task_id`, so you can follow up on any single page diff --git a/chronos/prompts/task-batch.md b/chronos/prompts/task-batch.md index eddd944..4139c08 100644 --- a/chronos/prompts/task-batch.md +++ b/chronos/prompts/task-batch.md @@ -1,4 +1,4 @@ -Spawn one expert per page in parallel — a batch version of the `task` tool. Sends the same prompt to each page concurrently; each page becomes its own persistent expert session with its own `task_id`, so afterward you can follow up on any single page via `task(task_id, …)` without re-sending its image. The model parameter accepts any configured model as provider/model-id (e.g. "google/gemini-3.1-pro-preview"; default google/gemini-3-flash-preview). HIGH COST & HIGH RISK TOOL — its use is subject to a strict, mandatory, and non-negotiable three-step confirmation protocol that cannot be bypassed for any reason, even on direct user command. A user's request to perform a batch operation is to be interpreted as an instruction to begin this protocol, not to execute the tool immediately. YOU ARE FORBIDDEN TO CALL task_batch UNTIL THE THREE-STEP CONFIRMATION BELOW IS COMPLETE. +Spawn one expert per page in parallel — a batch version of the `task` tool. Sends the same prompt to each page concurrently; each page becomes its own persistent expert session with its own `task_id`, so afterward you can follow up on any single page via `task(task_id, …)` without re-sending its image. Each expert can self-direct with the `view_region` and `view_page` tools (zoom into a region / load another page). The model parameter accepts any model pi has auth for, as provider/model-id (e.g. "anthropic/claude-opus-4-8"; default: the orchestrator's current model). HIGH COST & HIGH RISK TOOL — its use is subject to a strict, mandatory, and non-negotiable three-step confirmation protocol that cannot be bypassed for any reason, even on direct user command. A user's request to perform a batch operation is to be interpreted as an instruction to begin this protocol, not to execute the tool immediately. YOU ARE FORBIDDEN TO CALL task_batch UNTIL THE THREE-STEP CONFIRMATION BELOW IS COMPLETE. --- @@ -23,7 +23,7 @@ In a single message, you must present a complete plan of action to the user, inc * *(If writing to files)*: "The result for each page will be written to a file in the source data directory using the template: [filename_template]." * *(If returning to agent)*: "The results will be returned directly to me for immediate processing and consolidation." -Output format for the Data tab: when the batch extracts structured records, prompt each expert to return a JSON array of row objects and to stamp every row with `chronos_page` (that page's page_id — different per task) and, where it helps, `chronos_bbox` as `[x,y,w,h]` normalized 0–1. The Chronos Data tab then renders each output file as a table whose rows link back to their source page/region. (See "Structured data output" in the system prompt.) +Output format for the Data tab: when the batch extracts structured records, prompt each expert to return a JSON array of row objects and to stamp every row with `chronos_page` (that page's page_id — different per task) and, where it helps, `chronos_bbox` as `[x,y,w,h]` normalized 0–1. A row that cites several regions/pages may make these keys lists (aligned by index). The Chronos Data tab then renders each output file as a table whose rows link back to their source page/region. (See "Structured data output" in the system prompt.) Step 2: Request for Final Go/No-Go Confirmation End your proposal with a direct, unambiguous question requiring a final confirmation from the user. For example: diff --git a/chronos/prompts/task.md b/chronos/prompts/task.md index 4578472..7b07d3e 100644 --- a/chronos/prompts/task.md +++ b/chronos/prompts/task.md @@ -1 +1 @@ -Talk to an expert model in a persistent conversation. Omit task_id to spawn a new expert: the result ends with a `task_id:` line — pass that id back to ask the same expert follow-up questions without re-sending earlier images (e.g. "What did that abbreviation mean?"). Multiple experts can be active at once, each with its own task_id and conversation history. page_id: optionally attach a page image (file-system index, e.g. 1 for page_0001.png — not the printed page number) on spawn or on any follow-up ("now compare with page 43"); omit it for a text-only message. prompt: what to ask. model: any configured model as provider/model-id (default: google/gemini-3-flash-preview; pick a stronger model like google/gemini-3.1-pro-preview for difficult pages — an unknown model errors with the list of available models). On follow-ups the session's current model is reused unless you override it. output_file: if provided, write the response to this file in the source directory and return a short confirmation instead of the full text. When the output is structured records meant for the Data tab, prompt the expert to return a JSON array of row objects and to stamp each row with `chronos_page` (this page_id) — and optionally `chronos_bbox` as `[x,y,w,h]` normalized 0–1 — so the viewer can link each row back to its source region (see "Structured data output" in the system prompt). +Talk to an expert model in a persistent conversation. Omit task_id to spawn a new expert: the result ends with a `task_id:` line — pass that id back to ask the same expert follow-up questions without re-sending earlier images (e.g. "What did that abbreviation mean?"). Multiple experts can be active at once, each with its own task_id and conversation history. The expert can also self-direct: it has `view_region` (zoom into a sub-region) and `view_page` (load another page from the source) tools, so you don't have to predict the exact crop up front — give it the page and let it zoom in where it needs to. page_id: optionally attach a page image (file-system index, e.g. 1 for page_0001.png — not the printed page number) on spawn or on any follow-up ("now compare with page 43"); omit it for a text-only message. prompt: what to ask. model: any model pi has auth for, as provider/model-id (default: the orchestrator's current model; an unknown model errors with the list of available models). On follow-ups the session's current model is reused unless you override it. output_file: if provided, write the response to this file in the source directory and return a short confirmation instead of the full text. When the output is structured records meant for the Data tab, prompt the expert to return a JSON array of row objects and to stamp each row with `chronos_page` (this page_id) — and optionally `chronos_bbox` as `[x,y,w,h]` normalized 0–1 — so the viewer can link each row back to its source region. A row that draws on several places may use lists for these keys (e.g. `chronos_page: [42, 43]`, or several `chronos_bbox` regions on one page); see "Structured data output" in the system prompt. diff --git a/chronos/tools/expert-tools.ts b/chronos/tools/expert-tools.ts new file mode 100644 index 0000000..9ce4d05 --- /dev/null +++ b/chronos/tools/expert-tools.ts @@ -0,0 +1,165 @@ +/** + * Tools the vision-expert subagents (task / task_batch) can call during a turn. + * + * Experts run an agentic loop in `runExpertTurn`: they receive the page image + * the orchestrator chose, then may self-direct — zoom into a sub-region they + * only identify after seeing the page (`view_region`), or pull in another page + * from the same source (`view_page`). Both reuse the existing crop/read paths, + * so a tool result is just another image block appended to the conversation. + * + * The `Tool` shape pi-ai consumes is only `{ name, description, parameters }` — + * execution is the caller's job, handled by `executeExpertTool` below. + */ +import { Type } from "@sinclair/typebox"; +import type { ImageContent, TextContent, Tool, ToolCall, ToolResultMessage } from "@mariozechner/pi-ai"; +import type { Bbox } from "../utils/crop-image.js"; +import { pageImageContent } from "./expert-turn.js"; + +const bboxSchema = Type.Object({ + x: Type.Number({ minimum: 0, maximum: 1 }), + y: Type.Number({ minimum: 0, maximum: 1 }), + w: Type.Number({ minimum: 0, maximum: 1 }), + h: Type.Number({ minimum: 0, maximum: 1 }), +}); + +/** Tool definitions handed to `complete()` so the expert can request more imagery. */ +export const EXPERT_TOOLS: Tool[] = [ + { + name: "view_region", + description: + "Zoom into a sub-region of a page at full resolution. Use this to read dense tables, " + + "marginalia, or faint/damaged ink that is too small in the full-page view. bbox is the " + + "crop in normalized coordinates (0–1): x/y is the top-left corner, w/h is width/height. " + + "page_id is optional — omit it to zoom into the page you are currently looking at.", + parameters: Type.Object({ + bbox: bboxSchema, + page_id: Type.Optional( + Type.Number({ description: "Page to crop (file-system index). Omit to use the current page." }), + ), + }), + }, + { + name: "view_page", + description: + "Load another full page from the same source into the conversation — e.g. to compare with a " + + "neighbouring page or follow a record that continues overleaf. page_id is the file-system index " + + "(1 = page_0001.png), not the printed page number.", + parameters: Type.Object({ + page_id: Type.Number({ description: "Page to load (file-system index)." }), + }), + }, +]; + +/** Provenance of an image returned by a tool, so persistence can re-crop it from disk on restore. */ +export interface ExpertToolImageRef { + pageId: number; + bbox?: Bbox; + sourceDir: string; +} + +/** A tool result captured for persistence: text plus an optional re-hydratable image. */ +export interface PersistedToolResult { + toolCallId: string; + toolName: string; + isError: boolean; + text: string; + image?: ExpertToolImageRef; +} + +export interface ExpertToolOutcome { + message: ToolResultMessage; + persist: PersistedToolResult; + /** Page the expert is now looking at, so a later view_region can default to it. */ + viewedPageId?: number; +} + +function coerceBbox(value: unknown): Bbox | null { + if (!value || typeof value !== "object") return null; + const o = value as Record; + if (["x", "y", "w", "h"].every((k) => typeof o[k] === "number")) { + return { x: o.x as number, y: o.y as number, w: o.w as number, h: o.h as number }; + } + return null; +} + +/** + * Execute one expert tool call against the current source. Never throws: a bad + * bbox / missing page / absent source comes back as an `isError` tool result so + * the loop can feed it to the model and let it recover (and count toward the cap). + */ +export async function executeExpertTool( + call: ToolCall, + ctx: { sourceDir: string | undefined; currentPageId: number | null }, +): Promise { + const base = { + role: "toolResult" as const, + toolCallId: call.id, + toolName: call.name, + timestamp: Date.now(), + }; + const fail = (text: string): ExpertToolOutcome => ({ + message: { ...base, content: [{ type: "text", text }], isError: true }, + persist: { toolCallId: call.id, toolName: call.name, isError: true, text }, + }); + + if (!ctx.sourceDir) return fail("No source is active, so page imagery cannot be loaded."); + + if (call.name === "view_page" || call.name === "view_region") { + const isRegion = call.name === "view_region"; + let bbox: Bbox | undefined; + if (isRegion) { + const parsed = coerceBbox(call.arguments?.bbox); + if (!parsed) return fail("view_region requires bbox as { x, y, w, h }, each normalized 0–1."); + bbox = parsed; + } + const rawPage = call.arguments?.page_id; + const pageId = + rawPage !== undefined && rawPage !== null ? Math.round(Number(rawPage)) : ctx.currentPageId; + if (pageId === null || !Number.isFinite(pageId)) { + return fail(`${call.name} needs a page_id (no page is currently in view).`); + } + try { + const image: ImageContent = await pageImageContent(ctx.sourceDir, pageId, bbox); + const text = isRegion + ? `Zoomed into the requested region of page ${pageId}.` + : `Loaded page ${pageId}.`; + const content: (ImageContent | TextContent)[] = [image, { type: "text", text }]; + return { + message: { ...base, content, isError: false }, + persist: { + toolCallId: call.id, + toolName: call.name, + isError: false, + text, + image: { pageId, bbox, sourceDir: ctx.sourceDir }, + }, + viewedPageId: pageId, + }; + } catch (e) { + return fail((e as Error).message); + } + } + + return fail(`Unknown tool "${call.name}".`); +} + +/** Rebuild a persisted tool result on session restore, re-cropping its image from disk. */ +export async function rehydrateToolResult(tr: PersistedToolResult): Promise { + const content: (ImageContent | TextContent)[] = []; + if (tr.image) { + try { + content.push(await pageImageContent(tr.image.sourceDir, tr.image.pageId, tr.image.bbox)); + } catch { + // page/source no longer on disk — restore text-only + } + } + content.push({ type: "text", text: tr.text }); + return { + role: "toolResult", + toolCallId: tr.toolCallId, + toolName: tr.toolName, + content, + isError: tr.isError, + timestamp: Date.now(), + }; +} diff --git a/chronos/tools/expert-turn.ts b/chronos/tools/expert-turn.ts index f6bc36d..7a380c8 100644 --- a/chronos/tools/expert-turn.ts +++ b/chronos/tools/expert-turn.ts @@ -1,5 +1,12 @@ import { readFileSync, existsSync } from "node:fs"; -import { complete, type ImageContent, type Message, type TextContent, type UserMessage } from "@mariozechner/pi-ai"; +import { + complete, + type ImageContent, + type Message, + type TextContent, + type ToolCall, + type UserMessage, +} from "@mariozechner/pi-ai"; import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; import { pageIdToPath } from "../utils/page-files.js"; import type { ExpertRegistry, ExpertSession } from "./expert-registry.js"; @@ -8,9 +15,11 @@ import type { SourceContext } from "./source-context.js"; import { requireSource } from "./source-context.js"; import { resolveExpertModel } from "../utils/resolve-model.js"; import { cropImageToBase64, type Bbox } from "../utils/crop-image.js"; -import { appendExpertTurn, type PersistedExpert } from "../utils/expert-store.js"; +import { appendExpertTurn, type PersistedExpert, type PersistedStep } from "../utils/expert-store.js"; +import { EXPERT_TOOLS, executeExpertTool, rehydrateToolResult } from "./expert-tools.js"; -export const DEFAULT_EXPERT_MODEL = "google/gemini-3-flash-preview"; +// Bound the per-turn agentic loop so a confused expert can't spin on tool calls. +const MAX_EXPERT_TOOL_CALLS = 8; export function modelSpec(m: { provider: string; id: string }): string { return `${m.provider}/${m.id}`; @@ -34,20 +43,28 @@ export interface ExpertTurnInput { /** Continue an existing session; omit to spawn a new one. */ taskId?: string; prompt: string; - /** provider/model-id; defaults to the session's model on follow-up, else DEFAULT_EXPERT_MODEL. */ + /** provider/model-id; defaults to the session's model on follow-up, else the orchestrator's current model. */ model?: string; /** Attach this page's image to the message. */ pageId?: number; bbox?: Bbox; + /** Abort the (multi-call) agentic loop when the user cancels. */ + signal?: AbortSignal; } export type ExpertTurnResult = | { ok: true; taskId: string; model: string; text: string; cost: number | undefined; pageId: number | null } | { ok: false; error: string; taskId?: string }; +function isToolCall(c: { type: string }): c is ToolCall { + return c.type === "toolCall"; +} + /** * Run one expert turn: resolve the model, build the (optionally image-bearing) - * user message, call the model, and persist the exchange in the registry. + * user message, then run an agentic loop — the model may call `view_region` / + * `view_page` to pull in more imagery before answering. The full exchange + * (intermediate tool calls + results) is kept in the session and persisted. * Shared by the `task` tool (single, formatted) and `task_batch` (many). */ export async function runExpertTurn( @@ -88,10 +105,19 @@ export async function runExpertTurn( return { ok: false, taskId, error: (e as Error).message }; } turnSourceDir = sourceDir; + } else if (sourceCtx.sourceDir) { + // No image attached, but a source is active — let the expert's tools reach it. + turnSourceDir = sourceCtx.sourceDir; } content.push({ type: "text", text: input.prompt }); - const fallback = session ? modelSpec(session.model) : DEFAULT_EXPERT_MODEL; + // Default to the session's model on follow-up, else the orchestrator's current + // model (whatever the user has selected/authed in pi) — no provider is baked in. + const fallback = session + ? modelSpec(session.model) + : extCtx.model + ? modelSpec(extCtx.model) + : undefined; const resolved = await resolveExpertModel(input.model, extCtx.modelRegistry, fallback, pageId !== null); if (!resolved.ok) { return { ok: false, taskId, error: resolved.error }; @@ -107,47 +133,94 @@ export async function runExpertTurn( const userMessage: UserMessage = { role: "user", content, timestamp: Date.now() }; - const response = await complete( - resolved.model, - { systemPrompt: pageExpertPrompt, messages: [...session.messages, userMessage] }, - { apiKey: resolved.apiKey, headers: resolved.headers }, - ); - if (response.stopReason === "error") { - return { - ok: false, - taskId, - error: `Expert model error (${modelSpec(resolved.model)}): ${response.errorMessage ?? "unknown error"}`, - }; + // ── Agentic loop ───────────────────────────────────────────────────────── + // turnMessages accumulates everything this turn appends after the prior + // session history: the user message, intermediate tool calls/results, and the + // final answer. steps captures the intermediate exchange for persistence. + const turnMessages: Message[] = [userMessage]; + const steps: PersistedStep[] = []; + let currentPageId: number | null = pageId; + let toolCallCount = 0; + // Only offer the image-returning view tools to a model that can actually + // consume images; a text-only model just answers in text. + let toolsEnabled = resolved.model.input.includes("image"); + let totalCost = 0; + let finalResponse; + + for (;;) { + if (input.signal?.aborted) { + return { ok: false, taskId, error: "Expert turn aborted." }; + } + const response = await complete( + resolved.model, + { + systemPrompt: pageExpertPrompt, + messages: [...session.messages, ...turnMessages], + tools: toolsEnabled ? EXPERT_TOOLS : undefined, + }, + { apiKey: resolved.apiKey, headers: resolved.headers, signal: input.signal }, + ); + if (response.stopReason === "error") { + return { + ok: false, + taskId, + error: `Expert model error (${modelSpec(resolved.model)}): ${response.errorMessage ?? "unknown error"}`, + }; + } + turnMessages.push(response); + finalResponse = response; + totalCost += response.usage?.cost?.total ?? 0; + + const toolCalls = toolsEnabled ? response.content.filter(isToolCall) : []; + if (response.stopReason !== "toolUse" || toolCalls.length === 0) break; + + // Intermediate assistant turn — record it, then run each requested tool. + steps.push({ kind: "assistant", message: response }); + for (const call of toolCalls) { + toolCallCount++; + const outcome = await executeExpertTool(call, { sourceDir: turnSourceDir, currentPageId }); + turnMessages.push(outcome.message); + steps.push({ kind: "toolResult", toolResult: outcome.persist }); + if (outcome.viewedPageId !== undefined) currentPageId = outcome.viewedPageId; + } + // Spent the budget — drop tools so the next completion must answer in text. + if (toolCallCount >= MAX_EXPERT_TOOL_CALLS) toolsEnabled = false; } - session.messages.push(userMessage, response); - const text = response.content + session.messages.push(...turnMessages); + + const text = finalResponse.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join(""); // Persist this turn so the expert survives an agent restart. Compact: prompt + - // provenance + the text-only response; the page image is rehydrated from disk - // on restore. Best-effort — appendExpertTurn never throws. + // provenance + the (text-only) agentic exchange; page images are re-cropped + // from disk on restore. Best-effort — appendExpertTurn never throws. appendExpertTurn(extCtx.cwd, extCtx.sessionManager.getSessionId(), taskId!, modelSpec(resolved.model), { prompt: input.prompt, pageId: pageId ?? undefined, bbox: input.bbox, sourceDir: turnSourceDir, - response, + steps: steps.length > 0 ? steps : undefined, + response: finalResponse, }); - const cost = response.usage?.cost; - const costTotal = cost ? cost.input + cost.output + cost.cacheRead : undefined; - - return { ok: true, taskId: taskId!, model: modelSpec(resolved.model), text, cost: costTotal, pageId }; + return { + ok: true, + taskId: taskId!, + model: modelSpec(resolved.model), + text, + cost: totalCost > 0 ? totalCost : undefined, + pageId, + }; } /** * Rebuild in-memory expert sessions from their persisted turn-logs, re-cropping - * page images from disk. Skips a task whose model can no longer be resolved - * (e.g. missing API key); restores a turn text-only if its source page is gone. - * Mutates the registry in place (it's captured by reference by the tools). + * page images (and any tool-driven zoom crops) from disk. Skips a task whose + * model can no longer be resolved (e.g. missing API key); restores a turn + * text-only if its source page is gone. Mutates the registry in place. */ export async function restoreExpertSessions( registry: ExpertRegistry, @@ -168,9 +241,18 @@ export async function restoreExpertSessions( } content.push({ type: "text", text: turn.prompt }); const userMessage: UserMessage = { role: "user", content, timestamp: turn.response.timestamp }; - messages.push(userMessage, turn.response); + messages.push(userMessage); + // Replay the agentic exchange (assistant tool calls + re-hydrated results). + for (const step of turn.steps ?? []) { + if (step.kind === "assistant") { + messages.push(step.message); + } else { + messages.push(await rehydrateToolResult(step.toolResult)); + } + } + messages.push(turn.response); } - const resolved = await resolveExpertModel(rec.modelSpec, extCtx.modelRegistry, DEFAULT_EXPERT_MODEL, false); + const resolved = await resolveExpertModel(rec.modelSpec, extCtx.modelRegistry, undefined, false); if (!resolved.ok) continue; registry.sessions.set(rec.taskId, { messages, model: resolved.model }); const n = parseInt(rec.taskId.replace(/^task-/, ""), 10); diff --git a/chronos/tools/task-batch.ts b/chronos/tools/task-batch.ts index 8f38340..659ca6f 100644 --- a/chronos/tools/task-batch.ts +++ b/chronos/tools/task-batch.ts @@ -6,7 +6,7 @@ import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; import type { ToolText } from "../utils/tool-loader.js"; -import { runExpertTurn, DEFAULT_EXPERT_MODEL, type ExpertTurnInput } from "./expert-turn.js"; +import { runExpertTurn, type ExpertTurnInput } from "./expert-turn.js"; import { type Bbox } from "../utils/crop-image.js"; interface ExpertEntry { @@ -29,8 +29,9 @@ const taskBatchParams = Type.Object({ model: Type.Optional( Type.String({ description: - `Model as provider/model-id (e.g. "google/gemini-3.1-pro-preview"). ` + - `Default: ${DEFAULT_EXPERT_MODEL}. An unknown model errors with the list of available models.`, + `Model as provider/model-id (e.g. "anthropic/claude-opus-4-8"). ` + + `Default: the orchestrator's current model. Any model pi has auth for works; ` + + `an unknown model errors with the list of available models.`, }), ), output_file: Type.Optional( @@ -92,10 +93,10 @@ export function createTaskBatchTool( } const experts: ExpertEntry[] = []; - let resolvedModel = params.model ?? DEFAULT_EXPERT_MODEL; + let resolvedModel = params.model ?? "(orchestrator default)"; const runOne = async (pageId: number): Promise => { - const input: ExpertTurnInput = { prompt: params.prompt, model: params.model, pageId, bbox }; + const input: ExpertTurnInput = { prompt: params.prompt, model: params.model, pageId, bbox, signal }; const result = await runExpertTurn(registry, sourceCtx, pageExpertPrompt, extCtx, input); if (!result.ok) { return { page_id: pageId, status: "error", error: result.error }; diff --git a/chronos/tools/view-page.ts b/chronos/tools/view-page.ts index a7ff2e9..21628fb 100644 --- a/chronos/tools/view-page.ts +++ b/chronos/tools/view-page.ts @@ -5,10 +5,7 @@ import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; -import { runExpertTurn, DEFAULT_EXPERT_MODEL } from "./expert-turn.js"; - -// Re-export so existing importers (task-batch, extensions) keep working. -export { DEFAULT_EXPERT_MODEL } from "./expert-turn.js"; +import { runExpertTurn } from "./expert-turn.js"; const taskParams = Type.Object({ prompt: Type.String({ description: "What to ask the expert model." }), @@ -29,9 +26,9 @@ const taskParams = Type.Object({ model: Type.Optional( Type.String({ description: - `Model as provider/model-id (e.g. "google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-8"). ` + - `Default: ${DEFAULT_EXPERT_MODEL} for new tasks, the session's current model on follow-ups. ` + - `An unknown model errors with the list of available models.`, + `Model as provider/model-id (e.g. "anthropic/claude-opus-4-8"). ` + + `Default: the orchestrator's current model for new tasks, the session's current model on follow-ups. ` + + `Any model pi has auth for works; an unknown model errors with the list of available models.`, }) ), output_file: Type.Optional( @@ -65,13 +62,14 @@ export function createTaskTool( label: "Task", description, parameters: taskParams, - async execute(_toolCallId, params, _signal, _onUpdate, extCtx) { + async execute(_toolCallId, params, signal, _onUpdate, extCtx) { const result = await runExpertTurn(registry, sourceCtx, pageExpertPrompt, extCtx, { taskId: params.task_id, prompt: params.prompt, model: params.model, pageId: params.page_id, bbox: params.bbox, + signal, }); if (!result.ok) { diff --git a/chronos/utils/expert-store.ts b/chronos/utils/expert-store.ts index f85bb91..3008109 100644 --- a/chronos/utils/expert-store.ts +++ b/chronos/utils/expert-store.ts @@ -16,6 +16,17 @@ import { readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync } from import { join } from "node:path"; import type { AssistantMessage } from "@mariozechner/pi-ai"; import type { Bbox } from "./crop-image.js"; +import type { PersistedToolResult } from "../tools/expert-tools.js"; + +/** + * One agentic step within a turn: either an intermediate assistant message that + * requested tools, or the result of one such tool call. Tool-result images are + * stored as provenance refs (page/bbox/source), not base64 — they re-crop from + * disk on restore, like the initial page image. + */ +export type PersistedStep = + | { kind: "assistant"; message: AssistantMessage } + | { kind: "toolResult"; toolResult: PersistedToolResult }; export interface PersistedTurn { prompt: string; @@ -24,7 +35,13 @@ export interface PersistedTurn { bbox?: Bbox; /** Source dir the page came from, captured per-turn (the active source can change). */ sourceDir?: string; - /** The model's reply. Text-only for experts, so it serializes small. */ + /** + * Intermediate agentic steps (assistant tool calls + their results) that + * preceded the final response. Omitted for plain single-shot turns and for + * records written before experts could use tools. + */ + steps?: PersistedStep[]; + /** The model's final reply. Text-only, so it serializes small. */ response: AssistantMessage; } diff --git a/chronos/utils/resolve-model.ts b/chronos/utils/resolve-model.ts index dc3d3f5..9853779 100644 --- a/chronos/utils/resolve-model.ts +++ b/chronos/utils/resolve-model.ts @@ -25,6 +25,20 @@ async function resolveAuth( return { apiKey }; } +/** + * The cheapest model (by per-token input+output price) that pi has auth for, + * as "provider/model-id", or undefined if none. Used for throwaway helper calls + * like session-name summarisation — keep the cost negligible and provider-neutral. + */ +export function pickCheapestModelSpec(registry: ModelRegistry): string | undefined { + const models = registry.getAvailable(); + if (models.length === 0) return undefined; + const cheapest = [...models].sort( + (a, b) => (a.cost.input + a.cost.output) - (b.cost.input + b.cost.output), + )[0]; + return `${cheapest.provider}/${cheapest.id}`; +} + const MAX_LISTED_MODELS = 40; function formatAvailable(registry: ModelRegistry, needsVision: boolean): string { @@ -44,18 +58,26 @@ function formatAvailable(registry: ModelRegistry, needsVision: boolean): string /** * Resolve a model spec from the orchestrator into a pi model + API key. * - * Accepts "provider/model-id" (e.g. "google/gemini-3-flash-preview") or a bare + * Accepts "provider/model-id" (e.g. "anthropic/claude-opus-4-8") or a bare * model id, which is matched across all providers (ambiguous → error). + * `fallback` is used when no spec is given (e.g. the orchestrator's current + * model); if neither is set, the caller is told to choose a model explicitly. * When `needsVision` is set (an image is being attached), the model must * support image input. */ export async function resolveExpertModel( spec: string | undefined, registry: ModelRegistry, - fallback: string, + fallback: string | undefined, needsVision: boolean, ): Promise { - const requested = (spec ?? fallback).trim(); + const requested = (spec ?? fallback ?? "").trim(); + if (!requested) { + return { + ok: false, + error: `No model specified and no default is available. Pass a model as provider/model-id.\n${formatAvailable(registry, needsVision)}`, + }; + } let model: Model | undefined; const slash = requested.indexOf("/"); From 8d5e4a97d47631651bf552c8933a5e0e544cba61 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 09:21:45 +0200 Subject: [PATCH 02/17] feat(agent): auto-generate session titles with a cheap model (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On agent_end the pi-package summarises the session's USER prompts (only — never assistant replies, tool results, or page images) into a short title using the cheapest model pi has auth for, and caches it in a `.chronos/session-names.json` sidecar keyed by session id. The title is refined over the first few prompts then locked, with an in-process in-flight guard and atomic (temp+rename) writes. Best-effort and provider-agnostic: it falls back silently to the existing first-message truncation if no model is configured, the user is offline, or the call fails — naming never blocks the history list. The host overlays this name only when the user hasn't set one explicitly (precedence: user name > generated > first-message truncation). Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos/extensions/index.ts | 71 +++++++++++++++++++++++++++++ chronos/utils/session-name-store.ts | 68 +++++++++++++++++++++++++++ chronos/utils/session-namer.ts | 65 ++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 chronos/utils/session-name-store.ts create mode 100644 chronos/utils/session-namer.ts diff --git a/chronos/extensions/index.ts b/chronos/extensions/index.ts index b23bd89..04e77fa 100644 --- a/chronos/extensions/index.ts +++ b/chronos/extensions/index.ts @@ -20,6 +20,8 @@ import { listPageIds } from "../utils/page-files.js"; import { discoverSources } from "../utils/source-discovery.js"; import { ensureWorkspace } from "../utils/workspace.js"; import { saveSessionSource, loadSessionSource } from "../utils/session-source-store.js"; +import { getNamedPromptCount, saveSessionName } from "../utils/session-name-store.js"; +import { generateSessionTitle } from "../utils/session-namer.js"; import { connectHttp, sendToExtension, disconnectHttp } from "../http/http-client.js"; const __filename = fileURLToPath(import.meta.url); @@ -198,6 +200,14 @@ export default function (pi: ExtensionAPI) { disconnectHttp(); }); + // Auto-generate a short session title from the user's prompts (once, cached in + // a sidecar) so the history list shows something better than the truncated + // first message. Best-effort and non-blocking: fire-and-forget so it never + // delays the next prompt, and silently keeps the fallback on any failure. + pi.on("agent_end", async (_event, ctx) => { + void maybeNameSession(ctx); + }); + pi.on("session_directory", async (event) => { return { sessionDir: join(event.cwd, "sessions") }; }); @@ -243,6 +253,67 @@ function applySource(sourceCtx: SourceContext, workspaceDir: string, sourcePath: return true; } +// ── Session auto-naming ───────────────────────────────────────────────────── + +function textOfMessageContent(content: unknown): string | undefined { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + const parts = content + .filter((b): b is { type: "text"; text: string } => (b as any)?.type === "text") + .map((b) => b.text); + if (parts.length) return parts.join(" "); + } + return undefined; +} + +// Genuine user prompts from a session, in order. Skips the synthetic +// "Source selected: …" follow-up the /select-source command injects. +function userPromptsFromSession(entries: { type: string; [k: string]: any }[]): string[] { + const prompts: string[] = []; + for (const entry of entries) { + if (entry.type !== "message") continue; + const message = entry.message; + if (!message || message.role !== "user") continue; + const text = textOfMessageContent(message.content)?.trim(); + if (!text || text.startsWith("Source selected:")) continue; + prompts.push(text); + } + return prompts; +} + +// Refine the title over the first few prompts, then lock it, so a session named +// after one terse opener still gets a better label as its task takes shape. +const NAMING_PROMPT_CAP = 3; +// In-process guard so two near-simultaneous agent_end events don't both fire a +// generation before either has written its result. +const namingInFlight = new Set(); + +// Generate-and-cache a display name for the current session from its user +// prompts, unless the user named it explicitly or it's already named from at +// least this many prompts. Never throws. +async function maybeNameSession(ctx: ExtensionContext): Promise { + try { + const sessionId = ctx.sessionManager.getSessionId(); + if (!sessionId) return; + if (ctx.sessionManager.getSessionName()) return; // user set a name explicitly + if (namingInFlight.has(sessionId)) return; // a generation is already running + const prompts = userPromptsFromSession(ctx.sessionManager.getEntries()); + if (prompts.length === 0) return; + const target = Math.min(prompts.length, NAMING_PROMPT_CAP); + if (getNamedPromptCount(ctx.cwd, sessionId) >= target) return; // already named from enough prompts + + namingInFlight.add(sessionId); + try { + const title = await generateSessionTitle(ctx.modelRegistry, prompts); + if (title) saveSessionName(ctx.cwd, sessionId, title, target); + } finally { + namingInFlight.delete(sessionId); + } + } catch (err) { + console.warn("[chronos] session auto-name failed:", (err as Error).message); + } +} + // ── System prompt builder ────────────────────────────────────────────── function buildChronosSystemPrompt(sourceCtx: SourceContext, cwd: string): string { diff --git a/chronos/utils/session-name-store.ts b/chronos/utils/session-name-store.ts new file mode 100644 index 0000000..3f77823 --- /dev/null +++ b/chronos/utils/session-name-store.ts @@ -0,0 +1,68 @@ +/** + * Per-session auto-generated display names. + * + * Session names in the history list fall back to a truncation of the first user + * message, which is often a poor label. We generate a short title from the + * session's user prompts with a cheap model and cache it here, keyed by pi + * session id, so it isn't recomputed and the host's history scan can read it. + * + * Each entry records how many user prompts the name was generated from + * (`fromPrompts`), so the name can be refined as the session grows (up to a + * small cap) instead of being frozen on the very first prompt. + * + * Like the source sidecar, this is OUT-OF-BAND: it never touches the + * conversation history or pi's session file. Precedence in the UI is + * user-set name (pi session_info) > this generated name > first-message + * truncation. + */ +import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs"; +import { dirname, join } from "node:path"; + +interface NameEntry { + name: string; + /** How many user prompts the title was summarised from. */ + fromPrompts: number; +} + +function storePath(workspaceDir: string): string { + return join(workspaceDir, ".chronos", "session-names.json"); +} + +function readStore(workspaceDir: string): Record { + try { + const parsed = JSON.parse(readFileSync(storePath(workspaceDir), "utf-8")); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } +} + +/** How many user prompts the cached name was generated from (0 if none cached). */ +export function getNamedPromptCount(workspaceDir: string, sessionId: string): number { + if (!sessionId) return 0; + const entry = readStore(workspaceDir)[sessionId]; + return entry && typeof entry.fromPrompts === "number" ? entry.fromPrompts : 0; +} + +/** + * Cache (or refine) a generated display name. `fromPrompts` records how many + * prompts it summarises; a later call with more prompts overwrites it. Writes + * atomically (temp + rename) and re-reads immediately before writing so a + * concurrent writer's entry for a different session isn't clobbered. + */ +export function saveSessionName(workspaceDir: string, sessionId: string, name: string, fromPrompts: number): void { + if (!sessionId || !name) return; + const store = readStore(workspaceDir); + store[sessionId] = { name, fromPrompts }; + const path = storePath(workspaceDir); + mkdirSync(dirname(path), { recursive: true }); + const tmp = `${path}.${process.pid}.tmp`; + writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", "utf-8"); + renameSync(tmp, path); // atomic publish on the same filesystem +} + +/** The generated display name for this session, if any. */ +export function loadSessionName(workspaceDir: string, sessionId: string): string | undefined { + if (!sessionId) return undefined; + return readStore(workspaceDir)[sessionId]?.name; +} diff --git a/chronos/utils/session-namer.ts b/chronos/utils/session-namer.ts new file mode 100644 index 0000000..fc2546e --- /dev/null +++ b/chronos/utils/session-namer.ts @@ -0,0 +1,65 @@ +/** + * Summarise a session's user prompts into a short, descriptive title using the + * cheapest model pi has auth for. Best-effort and provider-agnostic: returns + * undefined (so the caller keeps the first-message fallback) on any failure — + * no models configured, offline, the call errors, or the output is unusable. + * + * Only USER prompts are sent — never assistant replies, tool results, or page + * images — so the input stays tiny, cheap, and free of extracted content. + */ +import { complete, type UserMessage } from "@mariozechner/pi-ai"; +import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { pickCheapestModelSpec, resolveExpertModel } from "./resolve-model.js"; + +const MAX_PROMPTS = 6; +const MAX_INPUT_CHARS = 4000; +const MAX_TITLE_CHARS = 80; + +const SYSTEM_PROMPT = + "You write concise titles for work sessions in a tool that extracts structured data from " + + "historical documents. Given the user's prompts from one session, reply with a single 3–8 " + + "word title in Title Case that captures the task (e.g. \"Extract Baptism Records, 1801–1815\"). " + + "Reply with the title only — no quotes, no trailing punctuation, no preamble."; + +function cleanTitle(raw: string): string | undefined { + let t = (raw.split("\n").find((l) => l.trim()) ?? "").trim(); + t = t.replace(/^["'`*]+/, "").replace(/["'`*]+$/, "").replace(/[.\s]+$/, "").trim(); + if (!t) return undefined; + return t.length > MAX_TITLE_CHARS ? t.slice(0, MAX_TITLE_CHARS).trim() : t; +} + +export async function generateSessionTitle( + registry: ModelRegistry, + userPrompts: string[], +): Promise { + const prompts = userPrompts.map((p) => p.trim()).filter(Boolean).slice(0, MAX_PROMPTS); + if (prompts.length === 0) return undefined; + + const spec = pickCheapestModelSpec(registry); + if (!spec) return undefined; + const resolved = await resolveExpertModel(spec, registry, undefined, false); + if (!resolved.ok) return undefined; + + const joined = prompts.map((p, i) => `${i + 1}. ${p}`).join("\n").slice(0, MAX_INPUT_CHARS); + const userMessage: UserMessage = { + role: "user", + content: `User prompts from this session:\n${joined}\n\nTitle:`, + timestamp: Date.now(), + }; + + try { + const response = await complete( + resolved.model, + { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, + { apiKey: resolved.apiKey, headers: resolved.headers }, + ); + if (response.stopReason === "error") return undefined; + const text = response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + return cleanTitle(text); + } catch { + return undefined; + } +} From 37a8f414a9e3ac89217040252b9f3e3e1e4247e6 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 09:21:53 +0200 Subject: [PATCH 03/17] =?UTF-8?q?feat(extension):=20session=20list=20?= =?UTF-8?q?=E2=80=94=20count=20user=20prompts,=20show=20generated=20names?= =?UTF-8?q?=20(#9,=20#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9: the "N messages" label in the history drawer now counts only USER prompts, not assistant/tool messages (which an agent that fans out many expert turns per prompt inflates meaninglessly). The cheap prefix scan still pre-filters by the `"role":"user"` substring, but now confirms the parsed role so a tool/assistant line that merely embeds that text isn't miscounted. #8 (host side): the session scan overlays the pi-package's auto-generated name (.chronos/session-names.json) as the display-name fallback, read fresh per listing so a late-written name isn't masked by the per-file mtime cache. Also extends the webview protocol (ChronosSessionInfo gains sessionId/userName; new loginRequired / viewer/clearSource / login messages used by #5/#6/#10). Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/src/panel/sessions.ts | 67 ++++++++++++++++---- chronos-vscode/src/panel/webview-protocol.ts | 11 ++++ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/chronos-vscode/src/panel/sessions.ts b/chronos-vscode/src/panel/sessions.ts index 3f456b4..f801e7d 100644 --- a/chronos-vscode/src/panel/sessions.ts +++ b/chronos-vscode/src/panel/sessions.ts @@ -56,15 +56,22 @@ function parseSessionFile(filePath: string): ChronosSessionInfo | undefined { let messageCount = 0; for (let i = 1; i < lines.length; i++) { const line = lines[i]; - if (line.startsWith('{"type":"message"')) { - messageCount++; - if (!firstUserMessage && line.includes('"role":"user"')) { - try { - const entry = JSON.parse(line); - firstUserMessage = firstTextOf(entry.message?.content)?.slice(0, 200); - } catch { - // skip malformed line + // Count only USER prompts — assistant/tool messages inflate the number on an + // agent that fans out many expert turns per prompt and tell the user nothing. + // The substring is just a cheap pre-filter (it avoids parsing the multi-MB + // base64 lines); confirm the real role so a tool/assistant line that merely + // embeds `"role":"user"` (e.g. an echoed sub-conversation) isn't miscounted. + if (line.startsWith('{"type":"message"') && line.includes('"role":"user"')) { + try { + const entry = JSON.parse(line); + if (entry.message?.role === "user") { + messageCount++; + if (!firstUserMessage) { + firstUserMessage = firstTextOf(entry.message?.content)?.slice(0, 200); + } } + } catch { + // skip malformed line } } else if (line.startsWith('{"type":"session_info"')) { try { @@ -78,16 +85,39 @@ function parseSessionFile(filePath: string): ChronosSessionInfo | undefined { const info: ChronosSessionInfo = { path: filePath, + // Sidecar-generated name is overlaid in scanDir (it can change after this + // file's mtime/size last did, so it must stay out of the per-file cache). name: name ?? firstUserMessage?.slice(0, 80) ?? "(empty session)", timestamp: Date.parse(header.timestamp) || stat.mtimeMs, messageCount, firstUserMessage, sizeBytes: stat.size, + sessionId: typeof header.id === "string" ? header.id : undefined, + userName: name, }; sessionInfoCache.set(filePath, { mtime: stat.mtimeMs, size: stat.size, info }); return info; } +// Auto-generated session names written by the pi-package (.chronos/session-names.json), +// keyed by pi session id. Read fresh per listing — the file is tiny and updates +// independently of the session JSONL, so it can't be folded into the file cache. +// Each value is `{ name, fromPrompts }` (a bare string is also accepted for +// forward/backward tolerance). +function readGeneratedNames(workspaceDir: string): Record { + try { + const parsed = JSON.parse(readFileSync(join(workspaceDir, ".chronos", "session-names.json"), "utf-8")); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } +} + +function generatedName(entry: { name?: string } | string | undefined): string | undefined { + if (typeof entry === "string") return entry || undefined; + return entry?.name || undefined; +} + // pi's default per-project session location: ~/.pi/agent/sessions/---- // (used by sessions created before the extension started pinning // PI_CODING_AGENT_SESSION_DIR to /sessions). @@ -96,7 +126,11 @@ function defaultPiSessionDir(workspaceDir: string): string { return join(homedir(), ".pi", "agent", "sessions", encoded); } -function scanDir(dir: string, sessions: Map): void { +function scanDir( + dir: string, + sessions: Map, + generatedNames: Record, +): void { let files: string[]; try { files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")); @@ -104,10 +138,16 @@ function scanDir(dir: string, sessions: Map): void { return; } for (const file of files) { - const info = parseSessionFile(join(dir, file)); + let info = parseSessionFile(join(dir, file)); // Key by file basename (timestamp_sessionId) so the same session never // shows twice if both locations somehow contain it - if (info && !sessions.has(file)) sessions.set(file, info); + if (!info || sessions.has(file)) continue; + // Overlay the auto-generated name when the user hasn't set one explicitly. + if (!info.userName && info.sessionId) { + const gen = generatedName(generatedNames[info.sessionId]); + if (gen) info = { ...info, name: gen }; + } + sessions.set(file, info); } } @@ -167,7 +207,8 @@ export function readSessionMessages(sessionFile: string): any[] | undefined { export function listSessions(workspaceDir: string): ChronosSessionInfo[] { const sessions = new Map(); - scanDir(join(workspaceDir, "sessions"), sessions); - scanDir(defaultPiSessionDir(workspaceDir), sessions); + const generatedNames = readGeneratedNames(workspaceDir); + scanDir(join(workspaceDir, "sessions"), sessions, generatedNames); + scanDir(defaultPiSessionDir(workspaceDir), sessions, generatedNames); return [...sessions.values()].sort((a, b) => b.timestamp - a.timestamp); } diff --git a/chronos-vscode/src/panel/webview-protocol.ts b/chronos-vscode/src/panel/webview-protocol.ts index 9586540..e821c2d 100644 --- a/chronos-vscode/src/panel/webview-protocol.ts +++ b/chronos-vscode/src/panel/webview-protocol.ts @@ -23,9 +23,14 @@ export interface ChronosSessionInfo { path: string; name: string; timestamp: number; + /** Count of USER prompts in the session (not assistant/tool messages). */ messageCount: number; firstUserMessage?: string; sizeBytes?: number; + /** pi session id (header.id) — keys the auto-name sidecar. */ + sessionId?: string; + /** Explicit user-set name (pi session_info), if any. Absent → name is a fallback. */ + userName?: string; } export type ExtToWebview = @@ -46,6 +51,10 @@ export type ExtToWebview = | { type: "sources"; sources: { name: string; pageCount: number }[] } | { type: "sessions"; sessions: ChronosSessionInfo[] } | { type: "resumeResult"; ok: boolean } + // auth: no models are available until the user connects a provider + | { type: "loginRequired"; required: boolean } + // new session: clear the viewer + source dropdown so display matches selection + | { type: "viewer/clearSource" } // viewer (image URI already resolved via asWebviewUri) | { type: "viewer/showPage"; @@ -79,6 +88,8 @@ export type WebviewToExt = | { type: "abort" } | { type: "restartAgent" } | { type: "newSession" } + // connect an AI provider: pick a provider, enter its API key, restart the agent + | { type: "login" } | { type: "resumeSession"; sessionPath: string } | { type: "setModel"; provider: string; modelId: string } | { type: "setThinkingLevel"; level: ThinkingLevel } From c5335ce45beea0567d2327a9ab709afd8eca6b36 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 09:22:08 +0200 Subject: [PATCH 04/17] feat(extension): native provider login, multi-reference rows, new-session reset (#6, #5, #10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three land together because they co-edit the panel, app, data-viewer, and styles. #6 — first-class login: pi's /login is TUI-only and unusable over RPC, so when no models are available the panel shows a discoverable CTA (header "Log in" button + empty-state banner). It opens a provider picker + API-key input, writes _API_KEY to .chronos/.env, and restarts the agent. Provider-agnostic; also exposed as the "Chronos: Connect AI Provider" command. Init Workspace no longer prompts for a Gemini key (#7), and the .env reader accepts mixed-case custom variable names. #5 — multi-reference provenance: a data row may cite several (page, bbox, source) locations via list-valued chronos_page/chronos_bbox/chronos_source; the viewer renders one citation chip per reference. Scalars still work (treated as a single reference) and a length-1 list broadcasts. Lists align by index, and an invalid page in the middle of a list no longer shifts the alignment of the others. #10 — new-session reset: on a fresh session the host clears the viewer + source dropdown (viewer/clearSource) so the displayed state matches the (unbound) selection, re-arming the Data-tab refresh guard and dropping a late preview that resolves after the clear. Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/package.json | 4 + chronos-vscode/src/extension.ts | 58 +++++----- chronos-vscode/src/panel/chronos-panel.ts | 100 +++++++++++++++++- chronos-vscode/test/suite.js | 37 +++++++ .../webview/components/chronos-app.ts | 42 +++++++- .../webview/components/data-viewer.ts | 100 ++++++++++++++---- .../webview/components/page-viewer.ts | 13 +++ chronos-vscode/webview/styles.css | 60 ++++++++++- 8 files changed, 362 insertions(+), 52 deletions(-) diff --git a/chronos-vscode/package.json b/chronos-vscode/package.json index 841c750..2646142 100644 --- a/chronos-vscode/package.json +++ b/chronos-vscode/package.json @@ -38,6 +38,10 @@ "command": "chronos.importSources", "title": "Chronos: Import Sources" }, + { + "command": "chronos.login", + "title": "Chronos: Connect AI Provider (Log In)" + }, { "command": "chronos.windowSetup", "title": "Chronos: Window Setup" diff --git a/chronos-vscode/src/extension.ts b/chronos-vscode/src/extension.ts index 8da2cd3..12d6b4e 100644 --- a/chronos-vscode/src/extension.ts +++ b/chronos-vscode/src/extension.ts @@ -163,7 +163,9 @@ function readEnvFile(envPath: string): Record { if (!existsSync(envPath)) return vars; const content = readFileSync(envPath, "utf-8"); for (const line of content.split("\n")) { - const match = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/); + // Accept mixed-case names so any var the login flow can write (the "Other + // provider…" path allows [A-Za-z_]…) round-trips on the next session. + const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); if (match) vars[match[1]] = match[2].trim(); } return vars; @@ -205,7 +207,7 @@ data/ Per-source extraction results and outputs memory/ Agent memory files (MEMORY.MD, per-source notes) skills/ Skill definitions (markdown task instructions) sessions/ Agent session logs (auto-generated) -.chronos/.env API keys (GEMINI_API_KEY) +.chronos/.env Provider API keys (set via "Chronos: Connect AI Provider") \`\`\` ## Getting Started @@ -234,38 +236,23 @@ sessions/ Agent session logs (auto-generated) 5. **Browse pages**: Run **"Chronos: Show Page"** to jump the viewer to a specific page. -## API Key +## AI provider -Your Gemini API key is stored in \`.chronos/.env\`. You can edit it there -or re-run **"Chronos: Init Workspace"** to set a new one. +Chronos works with any provider \`pi\` supports (Anthropic, Google, OpenAI, …). +Click **Log in** in the Chronos panel header — or run **"Chronos: Connect AI +Provider"** — to store an API key in \`.chronos/.env\`. You can also edit that +file directly (e.g. \`ANTHROPIC_API_KEY=...\`). ` ); - // Ask for Gemini API key - const envPath = join(folder, ".chronos", ".env"); - let apiKey: string | undefined; - - if (existsSync(envPath)) { - const overwrite = await vscode.window.showQuickPick(["Keep existing key", "Enter new key"], { - placeHolder: "A Gemini API key already exists. What would you like to do?", - }); - if (overwrite === "Enter new key") { - apiKey = await vscode.window.showInputBox({ - prompt: "Enter your Gemini API key", - password: true, - placeHolder: "AIza...", - }); - } - } else { - apiKey = await vscode.window.showInputBox({ - prompt: "Enter your Gemini API key (required for agent to work)", - password: true, - placeHolder: "AIza...", - }); - } - - if (apiKey !== undefined) { - writeFileSync(envPath, `GEMINI_API_KEY=${apiKey}\n`, "utf-8"); + // Auth is provider-agnostic and set up from the panel ("Log in" button, or the + // "Chronos: Connect AI Provider" command), which writes a _API_KEY + // into .chronos/.env. Just ensure the file exists so there's somewhere to write. + const chronosDir = join(folder, ".chronos"); + const envPath = join(chronosDir, ".env"); + if (!existsSync(envPath)) { + mkdirSync(chronosDir, { recursive: true }); + writeFileSync(envPath, "# Provider API keys, e.g. ANTHROPIC_API_KEY=... or GEMINI_API_KEY=...\n", "utf-8"); } return true; @@ -801,6 +788,17 @@ export function activate(context: vscode.ExtensionContext): { } }), + vscode.commands.registerCommand("chronos.login", async () => { + const panel = ChronosPanel.active; + if (!panel) { + vscode.window.showWarningMessage( + "Chronos: Start an agent session first (Chronos: Start Agent Session), then connect a provider." + ); + return; + } + await panel.promptLogin(); + }), + vscode.commands.registerCommand("chronos.windowSetup", async () => { // 1. Close all editor tabs await vscode.commands.executeCommand("workbench.action.closeAllEditors"); diff --git a/chronos-vscode/src/panel/chronos-panel.ts b/chronos-vscode/src/panel/chronos-panel.ts index ae3e473..647738b 100644 --- a/chronos-vscode/src/panel/chronos-panel.ts +++ b/chronos-vscode/src/panel/chronos-panel.ts @@ -21,6 +21,35 @@ function getNonce(): string { export type AgentStatus = "starting" | "ready" | "failed" | "exited"; +// Providers offered by the native login flow → the env var pi reads the key +// from (see pi-ai env-api-keys). "Other" lets the user name any other var. +const LOGIN_PROVIDERS: { label: string; envVar: string }[] = [ + { label: "Anthropic (Claude)", envVar: "ANTHROPIC_API_KEY" }, + { label: "Google (Gemini)", envVar: "GEMINI_API_KEY" }, + { label: "OpenAI", envVar: "OPENAI_API_KEY" }, + { label: "OpenRouter", envVar: "OPENROUTER_API_KEY" }, + { label: "xAI (Grok)", envVar: "XAI_API_KEY" }, + { label: "Groq", envVar: "GROQ_API_KEY" }, + { label: "Mistral", envVar: "MISTRAL_API_KEY" }, + { label: "DeepSeek", envVar: "DEEPSEEK_API_KEY" }, +]; + +// Set KEY=value in a .env file, replacing any existing assignment of KEY. +function upsertEnvVar(envPath: string, key: string, value: string): void { + mkdirSync(dirname(envPath), { recursive: true }); + const kept = existsSync(envPath) + ? readFileSync(envPath, "utf-8") + .split("\n") + .filter((line) => { + const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/); + return !(m && m[1] === key); + }) + : []; + while (kept.length && kept[kept.length - 1].trim() === "") kept.pop(); + kept.push(`${key}=${value}`); + writeFileSync(envPath, kept.join("\n") + "\n", "utf-8"); +} + // pi auto-registers every .md in a package's prompts/ dir as a slash command, // but the chronos package ships those files as agent-internal tool text (the // system prompt, expert prompts, per-tool descriptions) — not user commands. @@ -266,8 +295,18 @@ export class ChronosPanel { void this.postHistory(state); void this.rpc ?.request<{ models: ModelInfo[] }>({ type: "get_available_models" }) - .then((data) => this.post({ type: "models", models: data.models })) - .catch(() => {}); + .then((data) => { + const models = data.models ?? []; + this.post({ type: "models", models }); + // No models means no provider is authenticated — surface the login CTA. + this.post({ type: "loginRequired", required: models.length === 0 }); + }) + .catch(() => { + // The agent already started (rpc.start resolved), so a transient + // get_available_models failure shouldn't strand a stale "logged out" + // banner after a successful login — clear it; the next re-seed corrects it. + this.post({ type: "loginRequired", required: false }); + }); void this.rpc ?.request<{ commands: RpcSlashCommand[] }>({ type: "get_commands" }) .then((data) => this.post({ type: "commands", commands: (data.commands ?? []).filter(isUserCommand) })) @@ -340,6 +379,54 @@ export class ChronosPanel { this.post({ type: "agentRestarted" }); } + /** + * Connect an AI provider without leaving the panel: pick a provider, enter its + * API key, persist it to .chronos/.env, and restart the agent so pi picks it + * up. Provider-agnostic — pi reads _API_KEY from the subprocess env. + * (pi's own /login is a TUI-only flow with no RPC equivalent.) + */ + async promptLogin(): Promise { + const items: (vscode.QuickPickItem & { envVar: string })[] = [ + ...LOGIN_PROVIDERS.map((p) => ({ label: p.label, detail: p.envVar, envVar: p.envVar })), + { label: "Other provider…", detail: "Enter the API-key environment variable name", envVar: "" }, + ]; + const picked = await vscode.window.showQuickPick(items, { + placeHolder: "Connect an AI provider — the key is saved to .chronos/.env", + }); + if (!picked) return; + + let envVar = picked.envVar; + if (!envVar) { + envVar = ( + await vscode.window.showInputBox({ + prompt: "API-key environment variable name", + placeHolder: "e.g. TOGETHER_API_KEY", + }) + )?.trim() ?? ""; + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar)) return; + } + + const key = ( + await vscode.window.showInputBox({ + prompt: `Enter your ${picked.label} API key`, + password: true, + ignoreFocusOut: true, + }) + )?.trim(); + if (!key) return; + + try { + upsertEnvVar(join(this.workspaceDir, ".chronos", ".env"), envVar, key); + } catch (err) { + this.post({ type: "notify", level: "error", message: `Could not write .chronos/.env: ${(err as Error).message}` }); + return; + } + // Make the new key live for the next spawn and restart so models populate. + this.agentEnv[envVar] = key; + this.post({ type: "notify", level: "info", message: `Saved ${envVar}. Restarting agent…` }); + await this.restartAgent(); + } + // ── viewer ──────────────────────────────────────────────────────────────── /** Messages from the chronos pi-package arriving over HTTP. */ @@ -532,12 +619,21 @@ export class ChronosPanel { if (result && !result.cancelled) { this.currentSourceDir = undefined; this.currentSourceName = undefined; + // Re-arm the Data-tab refresh guard so re-selecting the same source + // (or restoring it on resume) repopulates the (now-cleared) Data tab. + this.lastDataSource = undefined; this.post({ type: "history", messages: [] }); + // The new session has no source bound — clear the viewer + dropdown so + // the display matches (don't leave the previous source showing). + this.post({ type: "viewer/clearSource" }); await this.refreshState(); this.postSessions(); } break; } + case "login": + await this.promptLogin(); + break; case "resumeSession": { try { const result = await this.rpc?.request<{ cancelled: boolean }>( diff --git a/chronos-vscode/test/suite.js b/chronos-vscode/test/suite.js index aa07bc0..4e196af 100644 --- a/chronos-vscode/test/suite.js +++ b/chronos-vscode/test/suite.js @@ -155,6 +155,43 @@ exports.run = async function run() { }); check("re-open icon on a tool entry restores the viewer", true); + // 7. Multi-reference provenance (#5): a row may cite several (page, bbox) + // locations via list-valued reserved keys; each should yield a citation. + writeFileSync( + join(dataDir, "multi.json"), + JSON.stringify( + [ + // two pages, two regions, one shared source — should parse as 2 refs + { name: "split entry", chronos_page: [1, 1], chronos_bbox: [[0.1, 0.1, 0.4, 0.1], [0.1, 0.3, 0.4, 0.1]], chronos_source: "TestSource" }, + // scalar (backward compatible) — 1 ref + { name: "single", chronos_page: 1, chronos_bbox: [0.2, 0.2, 0.3, 0.1] }, + ], + null, + 2, + ), + ); + api.chronosTest.invoke("selectDataFile", "multi.json"); + await waitFor("multi-reference row parsed", async () => { + const s = await dump(); + const counts = s?.data?.provenanceCounts || []; + return ( + s?.data?.selected === "multi.json" && + s?.data?.rowCount === 2 && + counts[0] === 2 && + counts[1] === 1 + ); + }); + check("data viewer renders multiple references per row", true); + + // 8. New session (#10): the viewer + source dropdown reset to "nothing + // selected" so the display matches the (unbound) selection. + api.chronosTest.invoke("newSession"); + await waitFor("new-session clears source/viewer", async () => { + const s = await dump(); + return s?.currentSource === "" && (s?.data?.sourceName ?? "") === "" && (s?.viewer?.sourceName ?? "") === ""; + }); + check("new session clears the viewer + source dropdown", true); + // Make sure the subprocess stayed alive throughout const after = api.getChronosStatus(); check("pi subprocess still alive", after?.agentStatus === "ready", after?.lastError); diff --git a/chronos-vscode/webview/components/chronos-app.ts b/chronos-vscode/webview/components/chronos-app.ts index a8467ae..b50f6aa 100644 --- a/chronos-vscode/webview/components/chronos-app.ts +++ b/chronos-vscode/webview/components/chronos-app.ts @@ -38,6 +38,7 @@ export class ChronosApp extends LitElement { contextTokens: { state: true }, sessionLoading: { state: true }, viewerTab: { state: true }, + loginRequired: { state: true }, }; declare state: RpcSessionState | null; @@ -53,6 +54,7 @@ export class ChronosApp extends LitElement { declare contextTokens: number; declare sessionLoading: { title?: string; name: string; sizeBytes?: number } | null; declare viewerTab: "page" | "data"; + declare loginRequired: boolean; private postMessage: (msg: WebviewToExt) => void = () => {}; private uiRequestQueue: RpcExtensionUIRequest[] = []; @@ -73,6 +75,7 @@ export class ChronosApp extends LitElement { this.contextTokens = 0; this.sessionLoading = null; this.viewerTab = "page"; + this.loginRequired = false; } // Context occupancy = what the next request will carry: fresh input + @@ -130,6 +133,16 @@ export class ChronosApp extends LitElement { case "models": this.models = msg.models; break; + case "loginRequired": + this.loginRequired = msg.required; + break; + case "viewer/clearSource": + // New session has no source bound — reset the display to match. + this.currentSource = ""; + this.viewer?.clearSource(); + this.dataViewer?.clearSource(); + this.viewerTab = "page"; + break; case "commands": this.chat?.setCommands(msg.commands); break; @@ -246,6 +259,9 @@ export class ChronosApp extends LitElement { case "openPageTab": this.viewerTab = "page"; break; + case "newSession": + this.postMessage({ type: "newSession" }); + break; case "selectDataFile": if (arg) this.dataViewer?.testSelect(arg); break; @@ -313,6 +329,7 @@ export class ChronosApp extends LitElement { return html`
{ if (this.viewerTab === "page") this.viewer?.handleKeydown(e); }}> ${this.renderHeader()} + ${this.loginRequired ? this.renderLoginBanner() : nothing}
@@ -379,7 +396,9 @@ export class ChronosApp extends LitElement { } }} > - ${!model ? html`` : nothing} + ${!model + ? html`` + : nothing} ${this.models.map((m) => { const id = `${m.provider}/${m.id}`; const current = model ? `${model.provider}/${model.id}` === id : false; @@ -388,6 +407,13 @@ export class ChronosApp extends LitElement { ${this.renderContextMeter()} + +
+ `; + } + private renderContextMeter(): TemplateResult | typeof nothing { const window = this.state?.model?.contextWindow; if (!window || !this.contextTokens) return nothing; diff --git a/chronos-vscode/webview/components/data-viewer.ts b/chronos-vscode/webview/components/data-viewer.ts index 7b1e65f..98d2412 100644 --- a/chronos-vscode/webview/components/data-viewer.ts +++ b/chronos-vscode/webview/components/data-viewer.ts @@ -31,10 +31,10 @@ interface Preview { } type Parsed = - | { kind: "table"; columns: string[]; rows: Row[]; provenance: (Provenance | null)[] } + | { kind: "table"; columns: string[]; rows: Row[]; provenance: Provenance[][] } | { kind: "text"; text: string }; -// Coerce a chronos_bbox value ([x,y,w,h] or {x,y,w,h}) to a Bbox, else null. +// Coerce a single chronos_bbox value ([x,y,w,h] or {x,y,w,h}) to a Bbox, else null. function toBbox(value: unknown): Bbox | null { if (Array.isArray(value) && value.length === 4 && value.every((n) => typeof n === "number")) { const [x, y, w, h] = value as number[]; @@ -49,12 +49,54 @@ function toBbox(value: unknown): Bbox | null { return null; } -function rowProvenance(row: Row): Provenance | null { - const raw = row[RESERVED.page]; - const pageId = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN; - if (isNaN(pageId)) return null; - const sourcePath = typeof row[RESERVED.source] === "string" ? (row[RESERVED.source] as string) : undefined; - return { pageId, bbox: toBbox(row[RESERVED.bbox]), sourcePath }; +// Normalize a scalar-or-list value to a list. A bare scalar becomes a single +// element; null/undefined becomes empty. (Bbox needs special handling — a single +// bbox is itself an array of 4 — so it is not routed through here.) +function toList(value: unknown): unknown[] { + if (value === undefined || value === null) return []; + return Array.isArray(value) ? value : [value]; +} + +// Length-preserving: invalid entries become NaN rather than being dropped, so a +// hole in chronos_page doesn't shift the index alignment with bbox/source. The +// NaN slot is skipped per-index in rowProvenance. +function pageList(value: unknown): number[] { + return toList(value).map((v) => (typeof v === "number" ? v : typeof v === "string" ? parseInt(v, 10) : NaN)); +} + +function sourceList(value: unknown): (string | undefined)[] { + return toList(value).map((v) => (typeof v === "string" ? v : undefined)); +} + +// chronos_bbox may be a single bbox ({x,y,w,h} or [x,y,w,h]) or a list of them. +function bboxList(value: unknown): (Bbox | null)[] { + if (value === undefined || value === null) return []; + const single = toBbox(value); + if (single) return [single]; + if (Array.isArray(value)) return value.map(toBbox); + return []; +} + +// A row may cite several (source, page, bbox) locations via list-valued reserved +// keys; a scalar is treated as a single-element list (backward compatible). Lists +// align by index; a length-1 list broadcasts (e.g. one source shared across pages, +// or several bboxes on a single page). A reference must resolve to a page id. +function rowProvenance(row: Row): Provenance[] { + const pages = pageList(row[RESERVED.page]); + const bboxes = bboxList(row[RESERVED.bbox]); + const sources = sourceList(row[RESERVED.source]); + const count = Math.max(pages.length, bboxes.length, sources.length); + const at = (list: T[], i: number): T | undefined => (list.length === 1 ? list[0] : list[i]); + + const refs: Provenance[] = []; + for (let i = 0; i < count; i++) { + const pageId = at(pages, i); + // Skip just this slot when the page is missing/invalid — keep every other + // reference aligned with its own bbox/source (don't shift the rest). + if (pageId === undefined || Number.isNaN(pageId)) continue; + refs.push({ pageId, bbox: at(bboxes, i) ?? null, sourcePath: at(sources, i) ?? undefined }); + } + return refs; } function formatCell(value: unknown): string { @@ -132,6 +174,16 @@ export class ChronosDataViewer extends LitElement { this.loading = false; } + /** Reset to the empty state (no source bound) — e.g. when a new session starts. */ + clearSource(): void { + this.sourceName = ""; + this.files = []; + this.selected = null; + this.content = null; + this.loading = false; + this.preview = null; + } + private selectFile(filename: string | null): void { this.selected = filename; this.content = null; @@ -180,7 +232,7 @@ export class ChronosDataViewer extends LitElement { return { kind: "table", columns, rows, provenance }; } - private sortRows(parsed: Extract): { rows: Row[]; provenance: (Provenance | null)[] } { + private sortRows(parsed: Extract): { rows: Row[]; provenance: Provenance[][] } { if (!this.sortColumn) return { rows: parsed.rows, provenance: parsed.provenance }; const col = this.sortColumn; const dir = this.sortDir; @@ -221,6 +273,10 @@ export class ChronosDataViewer extends LitElement { // Host resolved the page image — fill in the preview and crop it. showSourcePreview(imageUri: string, pageId: number, bbox: Bbox | null, sourceName: string): void { + // Drop a preview that resolved after the source was cleared (e.g. a new + // session cleared us while this request was in flight) — clearSource() sets + // sourceName to "", so an empty source means nothing should be shown. + if (!this.sourceName) return; this.preview = { ...(this.preview ?? {}), pageId, bbox, sourceName, imageUri }; void this.updateComplete.then(() => this.applyPreviewCrop()); } @@ -290,8 +346,8 @@ export class ChronosDataViewer extends LitElement { if (this.content === null) return; const parsed = this.parse(this.content); if (parsed.kind !== "table") return; - const prov = parsed.provenance.find((p): p is Provenance => p !== null); - if (prov) this.viewSource(prov); + const refs = parsed.provenance.find((r) => r.length > 0); + if (refs && refs[0]) this.viewSource(refs[0]); } testShowFullPage(): void { @@ -306,6 +362,7 @@ export class ChronosDataViewer extends LitElement { rowCount: number; columns: string[]; hasProvenance: boolean; + provenanceCounts: number[]; preview: { pageId: number; hasImage: boolean } | null; } { const parsed = this.content !== null ? this.parse(this.content) : null; @@ -316,7 +373,8 @@ export class ChronosDataViewer extends LitElement { kind: parsed?.kind ?? null, rowCount: parsed?.kind === "table" ? parsed.rows.length : 0, columns: parsed?.kind === "table" ? parsed.columns : [], - hasProvenance: parsed?.kind === "table" ? parsed.provenance.some(Boolean) : false, + hasProvenance: parsed?.kind === "table" ? parsed.provenance.some((r) => r.length > 0) : false, + provenanceCounts: parsed?.kind === "table" ? parsed.provenance.map((r) => r.length) : [], preview: this.preview ? { pageId: this.preview.pageId, hasImage: !!this.preview.imageUri } : null, }; } @@ -421,7 +479,7 @@ export class ChronosDataViewer extends LitElement { private renderTable(parsed: Extract): TemplateResult { const { rows, provenance } = this.sortRows(parsed); - const hasProvenance = provenance.some(Boolean); + const hasProvenance = provenance.some((refs) => refs.length > 0); return html` @@ -436,15 +494,21 @@ export class ChronosDataViewer extends LitElement { ${rows.map((row, i) => { - const prov = provenance[i]; + const refs = provenance[i]; return html` ${hasProvenance ? html`` : nothing} ${parsed.columns.map((c) => html``)} diff --git a/chronos-vscode/webview/components/page-viewer.ts b/chronos-vscode/webview/components/page-viewer.ts index 7e85d88..a65bb1a 100644 --- a/chronos-vscode/webview/components/page-viewer.ts +++ b/chronos-vscode/webview/components/page-viewer.ts @@ -89,6 +89,19 @@ export class ChronosPageViewer extends LitElement { this.lastPage = lastPage; } + /** Reset to the empty state (no source bound) — e.g. when a new session starts. + * Zoom is left alone: it's a UI preference, not source state. */ + clearSource(): void { + this.textView = null; + this.imageUri = ""; + this.pageId = 1; + this.sourceName = ""; + this.firstPage = 1; + this.lastPage = 1; + this.bbox = null; + this.showingCrop = false; + } + private get img(): HTMLImageElement | null { return this.querySelector("#pv-image"); } diff --git a/chronos-vscode/webview/styles.css b/chronos-vscode/webview/styles.css index 2bb4ea4..4d6d7d4 100644 --- a/chronos-vscode/webview/styles.css +++ b/chronos-vscode/webview/styles.css @@ -298,6 +298,57 @@ chronos-app, color: var(--ch-fg); } +/* Login button draws attention only while no provider is connected. */ +.header-btn.login-btn.is-attn { + color: var(--ch-accent-fg); + background: var(--ch-accent); +} + +.header-btn.login-btn.is-attn:hover { + background: color-mix(in srgb, var(--ch-accent) 85%, white); + color: var(--ch-accent-fg); +} + +/* ── "no models — connect a provider" banner (auth required) ──────────────── */ + +.login-banner { + display: flex; + align-items: center; + gap: 14px; + padding: 9px 16px; + background: color-mix(in srgb, var(--ch-accent) 12%, var(--ch-bg)); + border-bottom: 1px solid color-mix(in srgb, var(--ch-accent) 35%, transparent); + font-size: 12.5px; +} + +.login-banner-text { + flex: 1 1 auto; + color: var(--ch-fg); +} + +.login-banner-text code { + font-size: 11.5px; + padding: 1px 4px; + border-radius: var(--ch-radius-sm); + background: var(--ch-surface-2); +} + +.login-banner-btn { + flex: 0 0 auto; + height: 28px; + padding: 0 14px; + border-radius: var(--ch-radius-sm); + font-size: 12px; + font-weight: 600; + color: var(--ch-accent-fg); + background: var(--ch-accent); + transition: background 120ms ease; +} + +.login-banner-btn:hover { + background: color-mix(in srgb, var(--ch-accent) 85%, white); +} + #app-main { flex: 1 1 auto; display: flex; @@ -2510,7 +2561,14 @@ chronos-data-viewer, .dv-prov { text-align: center; - white-space: nowrap; +} + +/* A row can cite several source references — wrap the chips, don't overflow. */ +.dv-prov-list { + display: inline-flex; + flex-wrap: wrap; + gap: 3px; + justify-content: center; } .dv-view { From 7449bf62367ebb869ae67851d74c4259c20e5ec4 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 09:22:16 +0200 Subject: [PATCH 05/17] docs: provider-agnostic auth + model recommendations, expert tools, multi-reference rows (#5, #6, #7, #11) - #7/#6: drop "Gemini API key required" framing across README, the extension README, and DOCS; document the native "Log in" flow and the standard per-provider env vars. Model choice is the user's; recommendations (cheap model for routine pages, stronger for hard ones) now live in the docs, not in code. - #11: document the expert self-zoom tools (view_region / view_page) and the bounded tool loop. - #5: document list-valued provenance keys (multiple references per row) with examples and the index-alignment / broadcast rules. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 4 +-- DOCS.md | 55 ++++++++++++++++++++++++++++++++++------ README.md | 19 +++++++++----- chronos-vscode/README.md | 6 ++--- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a987fbc..7f461f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ Plain **Lit web components** (no React), bundled by esbuild to `out/webview/main `chronos/extensions/index.ts` is the pi-package entrypoint: it registers tools, the `/select-source` and `/yolo` commands, and lifecycle hooks. Tools live in `chronos/tools/` and share a mutable `SourceContext` (`tools/source-context.ts`) so `change_source` can redirect all source-bound tools at runtime. The system prompt is **rebuilt every turn** by the `before_agent_start` hook from `prompts/system-prompt.md` + the current `SourceContext` — it is never part of the persisted message history. -Key tools: `task`/`task_batch` (spawn persistent vision-expert subagents per page, follow-up-able via `task_id`), `list_pages`, `show_page`/`show_text` (viewer), `change_source`. Expert models are any `provider/model-id` pi has auth for (default `google/gemini-3-flash-preview`). +Key tools: `task`/`task_batch` (spawn persistent vision-expert subagents per page, follow-up-able via `task_id`; the expert runs a bounded tool loop and can `view_region`/`view_page` to self-zoom), `list_pages`, `show_page`/`show_text` (viewer), `change_source`. Expert models are any `provider/model-id` pi has auth for; no provider is hardcoded — they default to the orchestrator's current model. ### Important runtime facts @@ -69,7 +69,7 @@ Key tools: `task`/`task_batch` (spawn persistent vision-expert subagents per pag ### Workspace layout (user-facing, created by `Chronos: Init Workspace`) -A Chronos *workspace* (separate from this repo) contains `sources//png/page_NNNN.png`, `data/` (outputs), `memory/` (`MEMORY.MD` + per-source `.md`, injected into the system prompt), `skills//SKILL.md`, `sessions/`, and `.chronos/` (`.env` with `GEMINI_API_KEY`, `settings.json` for the `yolo` flag, `session-sources.json` mapping session id → selected source for resume). The workspace `skills/` dir is bridged into pi via `.pi/settings.json` (`{ "skills": ["../skills"] }`). +A Chronos *workspace* (separate from this repo) contains `sources//png/page_NNNN.png`, `data/` (outputs), `memory/` (`MEMORY.MD` + per-source `.md`, injected into the system prompt), `skills//SKILL.md`, `sessions/`, and `.chronos/` (`.env` with provider API keys e.g. `ANTHROPIC_API_KEY`/`GEMINI_API_KEY`, written by the panel's "Log in" flow; `settings.json` for the `yolo` flag; `session-sources.json` mapping session id → selected source for resume; `session-names.json` caching auto-generated session titles). The workspace `skills/` dir is bridged into pi via `.pi/settings.json` (`{ "skills": ["../skills"] }`). ## Reference diff --git a/DOCS.md b/DOCS.md index ba55b08..ad6b178 100644 --- a/DOCS.md +++ b/DOCS.md @@ -19,7 +19,7 @@ │ └── .md # Per-source findings ├── sessions/ # Conversation history (auto-generated) └── .chronos/ - └── .env # GEMINI_API_KEY + └── .env # provider API keys (e.g. ANTHROPIC_API_KEY, GEMINI_API_KEY) ``` ## Tools @@ -40,15 +40,33 @@ Standard file tools (`read`, `write`, `edit`, `grep`, `find`, `ls`) are also ava ### Expert models The `task` and `task_batch` tools accept any model pi has configured auth for, as -`provider/model-id` (default: `google/gemini-3-flash-preview`), e.g.: +`provider/model-id`. If you omit it, they default to the model selected in the panel +header (the orchestrator's current model) — no provider is hardcoded. Examples: +- `anthropic/claude-opus-4-8` - `google/gemini-3-flash-preview` - `google/gemini-3.1-pro-preview` -- `anthropic/claude-opus-4-8` +- `openai/gpt-...` + +The model must be vision-capable when a page image is attached. An unknown model name +errors with the list of available models. + +**Choosing a model** (recommendation, not a requirement): a fast/cheap vision model such +as `google/gemini-3-flash-preview` is a good default for routine pages; reach for a +stronger one (e.g. `google/gemini-3.1-pro-preview` or `anthropic/claude-opus-4-8`) on +dense tables, marginalia, or faint/damaged ink. Experts can also zoom in themselves (see +`view_region` / `view_page` below), which often matters more than raw model size. -An unknown model name errors with the list of available models. +Each expert keeps its own conversation (addressed by the `task_id` the tool returns), so you can ask follow-ups without re-sending the page image. These conversations are persisted per session under `.chronos/expert-sessions/`, so `task_id` follow-ups keep working after the agent restarts or a session is resumed. (Stored compactly — page images, including any tool-driven zoom crops, are re-read from disk on restore, not duplicated.) -Each expert keeps its own conversation (addressed by the `task_id` the tool returns), so you can ask follow-ups without re-sending the page image. These conversations are persisted per session under `.chronos/expert-sessions/`, so `task_id` follow-ups keep working after the agent restarts or a session is resumed. (Stored compactly — page images are re-read from disk on restore, not duplicated.) +#### Expert self-direction + +Experts aren't limited to the single (optionally pre-cropped) image the orchestrator hands them — they run a bounded agentic loop and can pull in more detail on demand: + +- **`view_region(bbox, [page_id])`** — crop a region of a page at full resolution (to read a dense table, marginal note, or faint ink). Omits `page_id` to zoom into the page currently in view. +- **`view_page(page_id)`** — load another full page from the same source (e.g. to follow a record overleaf). + +The loop is capped (currently 8 tool calls per turn) so a confused expert can't spin. This means you don't have to predict the right crop up front — pass the page and let the expert zoom where it needs to. ### Bounding box cropping @@ -70,9 +88,9 @@ To make each row traceable to its source page, include any of these reserved key | Key | Type | Meaning | |-----|------|---------| -| `chronos_page` | integer | Page the record was read from (same numbering as `show_page` / `[view p.N]`). | -| `chronos_bbox` | `[x, y, w, h]` (or `{x,y,w,h}`) | *Optional.* Region on that page, normalized 0–1. | -| `chronos_source` | string | *Optional.* Workspace-relative source path (e.g. `sources/Frankfurt_1864`) when the row is from a different source than the one in view. | +| `chronos_page` | integer **or list** | Page the record was read from (same numbering as `show_page` / `[view p.N]`). | +| `chronos_bbox` | `[x, y, w, h]` / `{x,y,w,h}` **or list** | *Optional.* Region on that page, normalized 0–1. | +| `chronos_source` | string **or list** | *Optional.* Workspace-relative source path (e.g. `sources/Frankfurt_1864`) when the row is from a different source than the one in view. | Example (`data/Frankfurt_1864/entries.json`): @@ -83,6 +101,27 @@ Example (`data/Frankfurt_1864/entries.json`): ] ``` +#### Multiple references per row + +A row can cite **more than one** source location — a value split across two pages, a figure assembled from several regions, or a fact corroborated by a marginal note. Pass the reserved keys as **parallel lists** and the Data tab renders one citation chip per reference, each linking to its own page/region: + +```json +[ + { + "name": "Anna Weber", + "chronos_page": [42, 43], + "chronos_bbox": [[0.10, 0.90, 0.80, 0.06], [0.10, 0.04, 0.80, 0.06]] + }, + { + "name": "Karl Vogt", + "chronos_page": 42, + "chronos_bbox": [[0.10, 0.32, 0.80, 0.05], [0.55, 0.32, 0.40, 0.05]] + } +] +``` + +The lists align by index. A scalar is treated as a single-element list (so existing single-reference outputs are unchanged), and a length-1 list **broadcasts** — e.g. one `chronos_source` shared across several pages, or several `chronos_bbox` regions on a single `chronos_page` (the second row above). A reference must resolve to a page id. + The keys are a recommendation — outputs without them still appear in the Data tab, just without click-to-source. ## Skills diff --git a/README.md b/README.md index 2decd0d..462a797 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An AI agent that collaborates with historians to extract structured datasets fro - [VS Code](https://code.visualstudio.com/) (v1.110+) - [Node.js](https://nodejs.org/) (v18+) — required by the underlying `pi` agent the extension installs on first run -- A [Gemini API key](https://aistudio.google.com/apikey) for the vision model +- An API key for an AI provider with a vision-capable model (Anthropic, Google, OpenAI, …). You connect it from inside the panel — see [Start the agent](#3-start-the-agent). ## Installation @@ -46,7 +46,7 @@ The `.vsix` is published on [GitHub Releases](https://github.com/ai-historian/ch ### 1. Initialize a workspace -Open VS Code in an empty folder. Press `Ctrl+Shift+P` and run **Chronos: Init Workspace**. This creates the workspace structure and prompts for your Gemini API key. +Open VS Code in an empty folder. Press `Ctrl+Shift+P` and run **Chronos: Init Workspace**. This creates the workspace structure. (You connect an AI provider in step 3 — no key is needed yet.) ### 2. Import sources @@ -60,27 +60,32 @@ Converting a large PDF can take a few minutes. Imports are crash-safe: a source Press `Ctrl+Shift+P` and run **Chronos: Start Agent Session**. The Chronos panel opens — a page viewer on the left and a chat on the right. -On first startup, type `/login` in the chat to log into your AI provider account (e.g. Anthropic, Google). Without this, no models will be available. +On first startup no AI models are available until you connect a provider. Click **Log in** in the panel header (or run **Chronos: Connect AI Provider**), pick your provider, and paste its API key. Chronos saves it to `.chronos/.env` and reconnects automatically. You can switch or add providers the same way at any time. Pick a source from the header dropdown (or type `/select-source`) and begin working. ## Configuration -### Environment variables +### AI provider & models -Set in `.chronos/.env`: +Connect a provider with the **Log in** button (above); it stores the key in `.chronos/.env`. You can also edit that file directly — pi reads the standard per-provider variables: ``` -GEMINI_API_KEY=your-key-here +ANTHROPIC_API_KEY=... # Claude +GEMINI_API_KEY=... # Google Gemini +OPENAI_API_KEY=... # OpenAI +# OPENROUTER_API_KEY, XAI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, … ``` +The page-analysis tools (`task` / `task_batch`) default to the model selected in the header, but accept any vision-capable model pi has auth for via a `model: "provider/model-id"` argument. Chronos is provider-agnostic — choose what fits your budget and accuracy needs. As a starting point, a fast/cheap vision model (e.g. `google/gemini-3-flash-preview`) works well for routine pages, and a stronger model (e.g. `google/gemini-3.1-pro-preview` or `anthropic/claude-opus-4-8`) helps on dense or damaged pages. + ### pi options pi supports many options natively. Common ones: ```bash # Use a specific model -pi --model gemini-2.5-pro +pi --model anthropic/claude-opus-4-8 # Continue previous session pi -c diff --git a/chronos-vscode/README.md b/chronos-vscode/README.md index 3823126..53a3ad9 100644 --- a/chronos-vscode/README.md +++ b/chronos-vscode/README.md @@ -8,7 +8,7 @@ An AI agent that collaborates with historians to extract structured datasets fro - VS Code v1.110+ - [Node.js](https://nodejs.org/) v18+ — required by the underlying `pi` agent the extension installs on first run -- A [Gemini API key](https://aistudio.google.com/apikey) for the vision model +- An API key for an AI provider with a vision-capable model (Anthropic, Google, OpenAI, …) — you connect it from the panel on first run ## Installation @@ -16,9 +16,9 @@ Install **Chronos — The AI Historian** from the Extensions view, then run any ## Getting started -1. **Chronos: Init Workspace** — open an empty folder and run this to create the workspace structure and set your Gemini API key. +1. **Chronos: Init Workspace** — open an empty folder and run this to create the workspace structure. 2. **Chronos: Import Sources** — add PDFs, images (PNG/JPG/TIFF/BMP), or text files. PDFs are converted to page images; imports are crash-safe and resumable. -3. **Chronos: Start Agent Session** — open the page viewer + chat and begin extracting data. +3. **Chronos: Start Agent Session** — open the page viewer + chat. Click **Log in** in the header to connect your AI provider (or run **Chronos: Connect AI Provider**), then begin extracting data. ## Documentation From a44c42be1c61531320955fc599b010f60caf52ac Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 10:08:36 +0200 Subject: [PATCH 06/17] feat(extension): in-panel Anthropic subscription (Claude Pro/Max) login (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the panel login beyond API keys: the "Log in" picker now offers "Anthropic — Claude Pro/Max (subscription)", which runs the OAuth flow directly from the host (pi's own /login is TUI-only and unreachable over RPC). A self-contained anthropic-oauth.ts module (no pi runtime dependency — constants copied verbatim from pi-ai, same as rpc-types.ts) does PKCE + authorize URL + the localhost callback server + token exchange. The flow opens the browser, auto-captures the redirect on localhost:53692 (with a paste-the-code fallback if the browser is remote or the port is busy), exchanges the code for tokens, and writes the credential to ~/.pi/agent/auth.json in pi's exact shape ({anthropic:{type:"oauth",...}}) — so the running pi reads and refreshes it. Then it restarts the agent and models populate. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 7 +- chronos-vscode/README.md | 2 +- chronos-vscode/src/anthropic-oauth.ts | 167 ++++++++++++++++++++++ chronos-vscode/src/panel/chronos-panel.ts | 147 +++++++++++++++++-- 4 files changed, 312 insertions(+), 11 deletions(-) create mode 100644 chronos-vscode/src/anthropic-oauth.ts diff --git a/README.md b/README.md index 462a797..bc8059b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,12 @@ Converting a large PDF can take a few minutes. Imports are crash-safe: a source Press `Ctrl+Shift+P` and run **Chronos: Start Agent Session**. The Chronos panel opens — a page viewer on the left and a chat on the right. -On first startup no AI models are available until you connect a provider. Click **Log in** in the panel header (or run **Chronos: Connect AI Provider**), pick your provider, and paste its API key. Chronos saves it to `.chronos/.env` and reconnects automatically. You can switch or add providers the same way at any time. +On first startup no AI models are available until you connect a provider. Click **Log in** in the panel header (or run **Chronos: Connect AI Provider**) and choose how to sign in: + +- **Anthropic — Claude Pro/Max (subscription):** signs in with your Claude subscription via OAuth in the browser — no API key needed. The credential is stored in pi's `~/.pi/agent/auth.json`. +- **API key (any provider):** paste a key; Chronos saves it to the workspace `.chronos/.env`. + +Either way Chronos reconnects automatically. You can switch or add providers the same way at any time. Pick a source from the header dropdown (or type `/select-source`) and begin working. diff --git a/chronos-vscode/README.md b/chronos-vscode/README.md index 53a3ad9..60d2fda 100644 --- a/chronos-vscode/README.md +++ b/chronos-vscode/README.md @@ -18,7 +18,7 @@ Install **Chronos — The AI Historian** from the Extensions view, then run any 1. **Chronos: Init Workspace** — open an empty folder and run this to create the workspace structure. 2. **Chronos: Import Sources** — add PDFs, images (PNG/JPG/TIFF/BMP), or text files. PDFs are converted to page images; imports are crash-safe and resumable. -3. **Chronos: Start Agent Session** — open the page viewer + chat. Click **Log in** in the header to connect your AI provider (or run **Chronos: Connect AI Provider**), then begin extracting data. +3. **Chronos: Start Agent Session** — open the page viewer + chat. Click **Log in** in the header (or run **Chronos: Connect AI Provider**) to connect a provider — sign in with a **Claude Pro/Max subscription** (OAuth) or paste an **API key** for any provider — then begin extracting data. ## Documentation diff --git a/chronos-vscode/src/anthropic-oauth.ts b/chronos-vscode/src/anthropic-oauth.ts new file mode 100644 index 0000000..4535821 --- /dev/null +++ b/chronos-vscode/src/anthropic-oauth.ts @@ -0,0 +1,167 @@ +// Anthropic (Claude Pro/Max) subscription login — OAuth authorization-code + PKCE. +// +// Kept self-contained so the extension has no runtime dependency on the pi +// packages (same rationale as src/rpc/rpc-types.ts). The constants below are +// copied verbatim from pi-ai's Anthropic OAuth provider +// (@…/pi-ai/dist/utils/oauth/anthropic.js) and MUST stay in sync with the +// installed pi: the credential we write to ~/.pi/agent/auth.json is read and +// refreshed by pi using the same CLIENT_ID, scopes, and token endpoint. +import { createServer } from "node:http"; + +// atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl") +const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; +const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; +const TOKEN_URL = "https://platform.claude.com/v1/oauth/token"; +const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1"; +const CALLBACK_PORT = 53692; +const CALLBACK_PATH = "/callback"; +const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; +const SCOPES = + "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"; + +export interface AnthropicOAuthCredentials { + refresh: string; + access: string; + expires: number; +} + +export interface AuthCode { + code: string; + state?: string; +} + +function base64url(bytes: Uint8Array): string { + let binary = ""; + for (const b of bytes) binary += String.fromCharCode(b); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + +async function generatePkce(): Promise<{ verifier: string; challenge: string }> { + const verifierBytes = new Uint8Array(32); + crypto.getRandomValues(verifierBytes); + const verifier = base64url(verifierBytes); + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)); + return { verifier, challenge: base64url(new Uint8Array(digest)) }; +} + +/** Build the authorize URL and the PKCE verifier (which doubles as the OAuth `state`). */ +export async function beginAnthropicLogin(): Promise<{ authUrl: string; verifier: string }> { + const { verifier, challenge } = await generatePkce(); + const params = new URLSearchParams({ + code: "true", + client_id: CLIENT_ID, + response_type: "code", + redirect_uri: REDIRECT_URI, + scope: SCOPES, + code_challenge: challenge, + code_challenge_method: "S256", + state: verifier, + }); + return { authUrl: `${AUTHORIZE_URL}?${params.toString()}`, verifier }; +} + +export interface CallbackServer { + /** Resolves with the browser-redirect code/state, or undefined once cancel() is called. */ + waitForCode(): Promise; + cancel(): void; + close(): void; +} + +const SUCCESS_HTML = + "" + + "

Signed in to Claude ✓

You can close this tab and return to VS Code.

"; +const FAIL_HTML = + "" + + "

Login failed

No authorization code was returned. Return to VS Code and try again.

"; + +/** + * Start the localhost callback server pi's redirect_uri points at. Rejects if + * the port is unavailable (the caller then falls back to manual code entry). + */ +export function startCallbackServer(): Promise { + return new Promise((resolve, reject) => { + let settle: (v: AuthCode | undefined) => void = () => {}; + const waitPromise = new Promise((r) => { + settle = r; + }); + const server = createServer((req, res) => { + const url = new URL(req.url || "", `http://${CALLBACK_HOST}:${CALLBACK_PORT}`); + if (url.pathname !== CALLBACK_PATH) { + res.writeHead(404).end(); + return; + } + const code = url.searchParams.get("code") ?? undefined; + const state = url.searchParams.get("state") ?? undefined; + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(code ? SUCCESS_HTML : FAIL_HTML); + if (code) settle({ code, state }); + }); + server.once("error", reject); + server.listen(CALLBACK_PORT, CALLBACK_HOST, () => { + resolve({ + waitForCode: () => waitPromise, + cancel: () => settle(undefined), + close: () => server.close(), + }); + }); + }); +} + +/** Parse a pasted authorization code, a "code#state" pair, or a full redirect URL. */ +export function parseAuthorizationInput(input: string): { code?: string; state?: string } { + const value = input.trim(); + if (!value) return {}; + try { + const url = new URL(value); + return { + code: url.searchParams.get("code") ?? undefined, + state: url.searchParams.get("state") ?? undefined, + }; + } catch { + // not a URL + } + if (value.includes("#")) { + const [code, state] = value.split("#", 2); + return { code, state }; + } + if (value.includes("code=")) { + const params = new URLSearchParams(value); + return { code: params.get("code") ?? undefined, state: params.get("state") ?? undefined }; + } + return { code: value }; +} + +/** Exchange the authorization code for OAuth tokens — the credential pi stores in auth.json. */ +export async function completeAnthropicLogin(args: { + code: string; + state: string; + verifier: string; +}): Promise { + const response = await fetch(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ + grant_type: "authorization_code", + client_id: CLIENT_ID, + code: args.code, + state: args.state, + redirect_uri: REDIRECT_URI, + code_verifier: args.verifier, + }), + signal: AbortSignal.timeout(30_000), + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(`Token exchange failed (HTTP ${response.status}): ${text}`); + } + const data = JSON.parse(text) as { access_token: string; refresh_token: string; expires_in: number }; + if (!data.access_token || !data.refresh_token) { + throw new Error("Token exchange returned no tokens."); + } + // Mirror pi's 5-minute safety margin on expiry. + return { + refresh: data.refresh_token, + access: data.access_token, + expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, + }; +} diff --git a/chronos-vscode/src/panel/chronos-panel.ts b/chronos-vscode/src/panel/chronos-panel.ts index 647738b..8e79aae 100644 --- a/chronos-vscode/src/panel/chronos-panel.ts +++ b/chronos-vscode/src/panel/chronos-panel.ts @@ -9,6 +9,15 @@ import type { AgentToExtensionMessage } from "../protocol"; import type { Bbox, ExtToWebview, WebviewToExt } from "./webview-protocol"; import { discoverSources, countPages } from "./sources"; import { listSessions, readSessionMessages } from "./sessions"; +import { + beginAnthropicLogin, + startCallbackServer, + completeAnthropicLogin, + parseAuthorizationInput, + type AnthropicOAuthCredentials, + type AuthCode, + type CallbackServer, +} from "../anthropic-oauth"; function getNonce(): string { let text = ""; @@ -50,6 +59,25 @@ function upsertEnvVar(envPath: string, key: string, value: string): void { writeFileSync(envPath, kept.join("\n") + "\n", "utf-8"); } +// Write an Anthropic OAuth credential into pi's global auth store +// (~/.pi/agent/auth.json), merging with any existing providers. This is the +// same file/shape pi's own /login writes, so pi reads and refreshes it. +function writeAnthropicOAuth(creds: AnthropicOAuthCredentials): void { + const authPath = join(homedir(), ".pi", "agent", "auth.json"); + let store: Record = {}; + if (existsSync(authPath)) { + try { + const parsed = JSON.parse(readFileSync(authPath, "utf-8")); + if (parsed && typeof parsed === "object") store = parsed as Record; + } catch { + store = {}; + } + } + store.anthropic = { type: "oauth", refresh: creds.refresh, access: creds.access, expires: creds.expires }; + mkdirSync(dirname(authPath), { recursive: true }); + writeFileSync(authPath, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 }); +} + // pi auto-registers every .md in a package's prompts/ dir as a slash command, // but the chronos package ships those files as agent-internal tool text (the // system prompt, expert prompts, per-tool descriptions) — not user commands. @@ -386,17 +414,37 @@ export class ChronosPanel { * (pi's own /login is a TUI-only flow with no RPC equivalent.) */ async promptLogin(): Promise { - const items: (vscode.QuickPickItem & { envVar: string })[] = [ - ...LOGIN_PROVIDERS.map((p) => ({ label: p.label, detail: p.envVar, envVar: p.envVar })), - { label: "Other provider…", detail: "Enter the API-key environment variable name", envVar: "" }, + type LoginItem = vscode.QuickPickItem & { + action: "oauth-anthropic" | "api-key" | "other"; + envVar?: string; + provider?: string; + }; + const items: LoginItem[] = [ + { + label: "Anthropic — Claude Pro/Max (subscription sign-in)", + detail: "OAuth — uses your Claude subscription, no API key", + action: "oauth-anthropic", + }, + ...LOGIN_PROVIDERS.map((p): LoginItem => ({ + label: `${p.label} — API key`, + detail: p.envVar, + action: "api-key", + envVar: p.envVar, + provider: p.label, + })), + { label: "Other provider — API key…", detail: "Enter the environment variable name", action: "other" }, ]; - const picked = await vscode.window.showQuickPick(items, { - placeHolder: "Connect an AI provider — the key is saved to .chronos/.env", - }); + const picked = await vscode.window.showQuickPick(items, { placeHolder: "Connect an AI provider" }); if (!picked) return; - let envVar = picked.envVar; - if (!envVar) { + if (picked.action === "oauth-anthropic") { + await this.loginAnthropicSubscription(); + return; + } + + let envVar = picked.envVar ?? ""; + let providerLabel = picked.provider ?? "provider"; + if (picked.action === "other") { envVar = ( await vscode.window.showInputBox({ prompt: "API-key environment variable name", @@ -404,11 +452,12 @@ export class ChronosPanel { }) )?.trim() ?? ""; if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar)) return; + providerLabel = envVar; } const key = ( await vscode.window.showInputBox({ - prompt: `Enter your ${picked.label} API key`, + prompt: `Enter your ${providerLabel} API key`, password: true, ignoreFocusOut: true, }) @@ -427,6 +476,86 @@ export class ChronosPanel { await this.restartAgent(); } + /** + * Anthropic subscription (Claude Pro/Max) login via OAuth, driven entirely + * from the panel: open the browser, capture the redirect on a localhost + * callback (or accept a pasted code), exchange it for tokens, write the + * credential to ~/.pi/agent/auth.json (what pi reads), and restart the agent. + */ + private async loginAnthropicSubscription(): Promise { + let codeRes: AuthCode | undefined; + let verifier = ""; + try { + const begun = await beginAnthropicLogin(); + verifier = begun.verifier; + let server: CallbackServer | undefined; + try { + server = await startCallbackServer(); + } catch { + server = undefined; // port unavailable → fall back to manual code entry + } + try { + codeRes = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, cancellable: true, title: "Connect Claude (Pro/Max)" }, + async (progress, token) => { + await vscode.env.openExternal(vscode.Uri.parse(begun.authUrl)); + progress.report({ + message: server + ? "Approve access in your browser — you'll return here automatically. Cancel to paste the code instead." + : "Approve access in your browser, then paste the code.", + }); + if (!server) return undefined; + token.onCancellationRequested(() => server!.cancel()); + return await server.waitForCode(); + }, + ); + } finally { + server?.close(); + } + } catch (err) { + this.post({ type: "notify", level: "error", message: `Claude sign-in failed: ${(err as Error).message}` }); + return; + } + + // No automatic redirect (cancelled, remote browser, or port busy) → paste it. + if (!codeRes) { + const pasted = ( + await vscode.window.showInputBox({ + prompt: "Paste the authorization code or the full redirect URL from your browser", + ignoreFocusOut: true, + }) + )?.trim(); + if (!pasted) return; // cancelled + const parsed = parseAuthorizationInput(pasted); + if (!parsed.code) { + this.post({ type: "notify", level: "error", message: "No authorization code found in the pasted value." }); + return; + } + if (parsed.state && parsed.state !== verifier) { + this.post({ type: "notify", level: "error", message: "OAuth state mismatch — please retry the sign-in." }); + return; + } + codeRes = { code: parsed.code, state: parsed.state ?? verifier }; + } + + let creds: AnthropicOAuthCredentials; + try { + creds = await completeAnthropicLogin({ code: codeRes.code, state: codeRes.state ?? verifier, verifier }); + } catch (err) { + this.post({ type: "notify", level: "error", message: `Claude sign-in failed: ${(err as Error).message}` }); + return; + } + + try { + writeAnthropicOAuth(creds); + } catch (err) { + this.post({ type: "notify", level: "error", message: `Could not write auth.json: ${(err as Error).message}` }); + return; + } + this.post({ type: "notify", level: "info", message: "Connected to Claude (Pro/Max). Restarting agent…" }); + await this.restartAgent(); + } + // ── viewer ──────────────────────────────────────────────────────────────── /** Messages from the chronos pi-package arriving over HTTP. */ From 743c389a8898cb53285c8c9e5641ced42bfd3dea Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 10:27:48 +0200 Subject: [PATCH 07/17] feat: surface expert (task) tool use with clickable viewer links (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Experts now run a tool loop (view_region/view_page), but that activity was invisible — the drawer only showed prompt/response. Now each runExpertTurn returns a compact `toolUses` summary (tool + page + bbox + isError), the task/task_batch result carries it in `details`, and the expert drawer renders each as an "examined" step. Region/page steps are clickable viewer links (reusing the data-page/data-bbox view-link path), so the historian can jump to exactly what the expert pulled in — full oversight of what the task did. Works live and on resumed sessions (details survive history). Guarded by a new UI test asserting the drawer renders the tool-use links. Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/test/suite.js | 9 +++ .../webview/components/chronos-app.ts | 3 + .../webview/components/chronos-chat.ts | 70 +++++++++++++++++++ chronos-vscode/webview/styles.css | 36 ++++++++++ chronos/tools/expert-turn.ts | 30 +++++++- chronos/tools/task-batch.ts | 7 +- chronos/tools/view-page.ts | 6 +- 7 files changed, 154 insertions(+), 7 deletions(-) diff --git a/chronos-vscode/test/suite.js b/chronos-vscode/test/suite.js index 4e196af..702889e 100644 --- a/chronos-vscode/test/suite.js +++ b/chronos-vscode/test/suite.js @@ -192,6 +192,15 @@ exports.run = async function run() { }); check("new session clears the viewer + source dropdown", true); + // 9. Expert oversight (#11): the task/task_batch drawer surfaces the expert's + // own view_region/view_page calls as clickable viewer links. + api.chronosTest.invoke("injectExpertTools"); + await waitFor("expert drawer shows clickable tool-use links", async () => { + const s = await dump(); + return s?.chat?.expertOpen === "task-1" && s?.chat?.expertToolLinks === 2; + }); + check("expert drawer surfaces tool-use viewer links", true); + // Make sure the subprocess stayed alive throughout const after = api.getChronosStatus(); check("pi subprocess still alive", after?.agentStatus === "ready", after?.lastError); diff --git a/chronos-vscode/webview/components/chronos-app.ts b/chronos-vscode/webview/components/chronos-app.ts index b50f6aa..efa96fb 100644 --- a/chronos-vscode/webview/components/chronos-app.ts +++ b/chronos-vscode/webview/components/chronos-app.ts @@ -262,6 +262,9 @@ export class ChronosApp extends LitElement { case "newSession": this.postMessage({ type: "newSession" }); break; + case "injectExpertTools": + this.chat?.testInjectExpertWithTools(); + break; case "selectDataFile": if (arg) this.dataViewer?.testSelect(arg); break; diff --git a/chronos-vscode/webview/components/chronos-chat.ts b/chronos-vscode/webview/components/chronos-chat.ts index defb4a1..081097c 100644 --- a/chronos-vscode/webview/components/chronos-chat.ts +++ b/chronos-vscode/webview/components/chronos-chat.ts @@ -47,6 +47,15 @@ type RenderBlock = // batch subagent call (`task_batch`) — one card listing the N experts it spawned | { kind: "expertBatch"; key: string; item: ToolItem }; +// A tool the expert invoked during a turn (view_region / view_page), surfaced +// from the task/task_batch result so the drawer can show what it inspected. +interface ExpertToolUse { + tool: string; + pageId?: number; + bbox?: { x: number; y: number; w: number; h: number }; + isError: boolean; +} + // One reconstructed turn of an expert conversation, gathered from `task` calls // and/or the first turn embedded in a `task_batch` result. interface ExpertTurn { @@ -56,6 +65,7 @@ interface ExpertTurn { reply: string; running: boolean; isError: boolean; + toolUses?: ExpertToolUse[]; } const SOURCE_SELECTED_RE = /^Source selected: "([^"]+)"/; @@ -139,6 +149,7 @@ interface BatchExpertEntry { response?: string; file?: string; error?: string; + toolUses?: ExpertToolUse[]; } /** Read a task_batch tool result's details (empty until the call completes). */ @@ -473,6 +484,8 @@ export class ChronosChat extends LitElement { toolNames: string[]; lastAssistant: string; running: boolean; + expertOpen: string | null; + expertToolLinks: number; } { const assistant = [...this.items].reverse().find((i) => i.kind === "assistant") as | { kind: "assistant"; message: AssistantMessage } @@ -483,9 +496,40 @@ export class ChronosChat extends LitElement { toolNames: this.items.filter((i) => i.kind === "tool").map((i) => (i as ToolItem).toolName), lastAssistant: assistant ? messageText(assistant.message.content) : "", running: this.running, + expertOpen: this.openExpert, + // Clickable viewer links for the expert's own tool calls, in the open drawer. + expertToolLinks: this.querySelectorAll(".expert-turn-tools a.view-link").length, }; } + // Test seam: inject a synthetic expert (task) call whose result carries + // tool-use oversight data, and open its drawer — exercises the expert + // tool-link rendering end-to-end without a live model. + testInjectExpertWithTools(): void { + this.items = [ + { + kind: "tool", + toolCallId: "tc-expert", + toolName: "task", + args: { prompt: "Read the marriage entries on this page.", page_id: 42 }, + result: { + content: [{ type: "text", text: "Found 3 entries.\ntask_id: task-1" }], + details: { + taskId: "task-1", + model: "anthropic/claude-opus-4-8", + toolUses: [ + { tool: "view_region", pageId: 42, bbox: { x: 0.1, y: 0.3, w: 0.8, h: 0.1 }, isError: false }, + { tool: "view_page", pageId: 43, isError: false }, + ], + }, + }, + isError: false, + running: false, + }, + ]; + this.openExpert = "task-1"; + } + private sendPrompt(): void { const input = this.querySelector("#composer-input"); if (!input) return; @@ -831,6 +875,7 @@ export class ChronosChat extends LitElement { reply: taskReplyText(item).replace(LEADING_VIEW_LINKS_RE, ""), running: !!item.running, isError: !!item.isError, + toolUses: (item.result as any)?.details?.toolUses, }); } else if (item.toolName === "task_batch") { const { prompt, bbox, experts } = batchDetails(item); @@ -843,6 +888,7 @@ export class ChronosChat extends LitElement { reply: entry.response ?? (entry.file ? `→ ${entry.file}` : entry.error ?? ""), running: false, isError: entry.status === "error", + toolUses: entry.toolUses, }); } } @@ -971,12 +1017,36 @@ export class ChronosChat extends LitElement { : nothing} ${turn.prompt} + ${turn.toolUses && turn.toolUses.length + ? html`
+ examined + ${turn.toolUses.map((u) => this.renderExpertToolUse(u))} +
` + : nothing} ${turn.running ? html`
thinking…
` : html`
${unsafeHTML(renderMarkdown(reply || "*(empty response)*"))}
`} `; } + // One expert tool call (view_region/view_page) as a clickable viewer link, so + // the historian can jump to exactly the page/region the expert pulled in. + private renderExpertToolUse(u: ExpertToolUse): TemplateResult { + const label = u.tool === "view_region" ? "⛶ region" : u.tool === "view_page" ? "page" : u.tool.replace(/_/g, " "); + if (u.pageId == null) { + return html`${label}`; + } + const bboxAttr = u.bbox ? `${u.bbox.x},${u.bbox.y},${u.bbox.w},${u.bbox.h}` : undefined; + return html`${label} p.${u.pageId}`; + } + private renderExpertBatchCard(item: ToolItem): TemplateResult { if (item.running) { return html` diff --git a/chronos-vscode/webview/styles.css b/chronos-vscode/webview/styles.css index 4d6d7d4..906b7c2 100644 --- a/chronos-vscode/webview/styles.css +++ b/chronos-vscode/webview/styles.css @@ -2131,6 +2131,42 @@ a.expert-tag.is-page:hover { font-size: 12px; } +/* What the expert pulled in (view_region/view_page) — clickable viewer links. */ +.expert-turn-tools { + align-self: flex-start; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px; + max-width: 94%; + padding-left: 13px; + margin: 1px 0; +} + +.expert-turn-tools-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ch-muted); +} + +.expert-tool { + font-size: 11px; + padding: 1px 7px; + border-radius: var(--ch-radius-sm); + font-variant-numeric: tabular-nums; +} + +span.expert-tool { + color: var(--ch-muted); + background: var(--ch-surface-2); +} + +.expert-tool.is-error { + color: var(--ch-error); + text-decoration: line-through; +} + .expert-turn-a { align-self: flex-start; max-width: 94%; diff --git a/chronos/tools/expert-turn.ts b/chronos/tools/expert-turn.ts index 7a380c8..dc27f7d 100644 --- a/chronos/tools/expert-turn.ts +++ b/chronos/tools/expert-turn.ts @@ -52,8 +52,25 @@ export interface ExpertTurnInput { signal?: AbortSignal; } +/** One tool the expert invoked during a turn — surfaced to the UI for oversight. */ +export interface ExpertToolUse { + tool: string; + pageId?: number; + bbox?: Bbox; + isError: boolean; +} + export type ExpertTurnResult = - | { ok: true; taskId: string; model: string; text: string; cost: number | undefined; pageId: number | null } + | { + ok: true; + taskId: string; + model: string; + text: string; + cost: number | undefined; + pageId: number | null; + /** view_region/view_page calls the expert made this turn (in order). */ + toolUses: ExpertToolUse[]; + } | { ok: false; error: string; taskId?: string }; function isToolCall(c: { type: string }): c is ToolCall { @@ -206,6 +223,16 @@ export async function runExpertTurn( response: finalResponse, }); + // Surface the expert's tool calls (page/region it pulled in) for UI oversight. + const toolUses: ExpertToolUse[] = steps + .filter((s): s is Extract => s.kind === "toolResult") + .map((s) => ({ + tool: s.toolResult.toolName, + pageId: s.toolResult.image?.pageId, + bbox: s.toolResult.image?.bbox, + isError: s.toolResult.isError, + })); + return { ok: true, taskId: taskId!, @@ -213,6 +240,7 @@ export async function runExpertTurn( text, cost: totalCost > 0 ? totalCost : undefined, pageId, + toolUses, }; } diff --git a/chronos/tools/task-batch.ts b/chronos/tools/task-batch.ts index 659ca6f..96115c9 100644 --- a/chronos/tools/task-batch.ts +++ b/chronos/tools/task-batch.ts @@ -6,7 +6,7 @@ import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; import type { ToolText } from "../utils/tool-loader.js"; -import { runExpertTurn, type ExpertTurnInput } from "./expert-turn.js"; +import { runExpertTurn, type ExpertTurnInput, type ExpertToolUse } from "./expert-turn.js"; import { type Bbox } from "../utils/crop-image.js"; interface ExpertEntry { @@ -17,6 +17,7 @@ interface ExpertEntry { file?: string; error?: string; cost?: number; + toolUses?: ExpertToolUse[]; } const taskBatchParams = Type.Object({ @@ -105,9 +106,9 @@ export function createTaskBatchTool( if (outputFileTemplate) { const filename = outputFileTemplate.replace("{page_id}", String(pageId).padStart(4, "0")); writeFileSync(join(outputDir, filename), result.text || "(empty response)", "utf-8"); - return { taskId: result.taskId, page_id: pageId, status: "ok", file: filename, cost: result.cost }; + return { taskId: result.taskId, page_id: pageId, status: "ok", file: filename, cost: result.cost, toolUses: result.toolUses }; } - return { taskId: result.taskId, page_id: pageId, status: "ok", response: result.text || "(empty response)", cost: result.cost }; + return { taskId: result.taskId, page_id: pageId, status: "ok", response: result.text || "(empty response)", cost: result.cost, toolUses: result.toolUses }; }; // Concurrency-limited worker pool. diff --git a/chronos/tools/view-page.ts b/chronos/tools/view-page.ts index 21628fb..fcee527 100644 --- a/chronos/tools/view-page.ts +++ b/chronos/tools/view-page.ts @@ -80,7 +80,7 @@ export function createTaskTool( }; } - const { taskId, model, text, cost, pageId } = result; + const { taskId, model, text, cost, pageId, toolUses } = result; const bbox = params.bbox ?? null; const costStr = cost !== undefined ? ` [cost: $${cost.toFixed(4)}]` : ""; @@ -99,13 +99,13 @@ export function createTaskTool( writeFileSync(outPath, text || "(empty response)", "utf-8"); return { content: [{ type: "text", text: `${viewLink}→ ${params.output_file}${trailer}` }], - details: { model, taskId, pageId, bbox, cost: costStr, path: outPath }, + details: { model, taskId, pageId, bbox, cost: costStr, path: outPath, toolUses }, }; } return { content: [{ type: "text", text: `${viewLink}${text || "(empty response)"}${trailer}` }], - details: { model, taskId, pageId, bbox, cost: costStr }, + details: { model, taskId, pageId, bbox, cost: costStr, toolUses }, }; }, }; From 648e780fb79d54359478bf81d84d7c560060d749 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 10:51:40 +0200 Subject: [PATCH 08/17] feat: read-only experts by default, human-gated bash/write/edit grant (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expert subagents now have a real capability model: - Read-only by default: view_region, view_page, read_file, list_dir, grep — the file tools are scoped to the workspace root (path-escape rejected). - The orchestrator can elevate a single task/task_batch call with grant: ["bash"|"write"|"edit"], adding bash / write_file / edit_file. This is OFF by default for oversight and safety, and requesting it triggers a user confirmation before any expert runs (once per task, once per batch cohort); denial aborts the call. executeExpertTool also refuses any elevated tool whose capability wasn't granted (defense in depth). Tool definitions are now built per-turn from (vision capability + granted caps), each call is captured with a short detail (command/path/term) and surfaced in the expert drawer: page steps stay clickable viewer links, file/search/command steps show as chips, and bash/write/edit are visually flagged so elevated actions are easy to audit. Prompts/docs explain that experts are read-only by default and that grants need user consent. Verified: agent-side integration test (15 cases incl. capability denial, path sandboxing, bash exec, write/edit) + the UI suite asserting the drawer renders links, chips, and the flagged elevated action. Co-Authored-By: Claude Opus 4.8 (1M context) --- DOCS.md | 19 +- chronos-vscode/test/suite.js | 11 +- .../webview/components/chronos-chat.ts | 52 ++- chronos-vscode/webview/styles.css | 7 + chronos/prompts/page-expert-prompt.md | 10 +- chronos/prompts/system-prompt.md | 8 +- chronos/prompts/task-batch.md | 2 +- chronos/prompts/task.md | 2 +- chronos/tools/expert-tools.ts | 432 ++++++++++++++---- chronos/tools/expert-turn.ts | 65 ++- chronos/tools/task-batch.ts | 31 +- chronos/tools/view-page.ts | 29 +- 12 files changed, 549 insertions(+), 119 deletions(-) diff --git a/DOCS.md b/DOCS.md index ad6b178..eb309ad 100644 --- a/DOCS.md +++ b/DOCS.md @@ -61,12 +61,23 @@ Each expert keeps its own conversation (addressed by the `task_id` the tool retu #### Expert self-direction -Experts aren't limited to the single (optionally pre-cropped) image the orchestrator hands them — they run a bounded agentic loop and can pull in more detail on demand: +Experts aren't limited to the single (optionally pre-cropped) image the orchestrator hands them — they run a bounded agentic loop (capped at 8 tool calls/turn). **By default they are read-only:** -- **`view_region(bbox, [page_id])`** — crop a region of a page at full resolution (to read a dense table, marginal note, or faint ink). Omits `page_id` to zoom into the page currently in view. -- **`view_page(page_id)`** — load another full page from the same source (e.g. to follow a record overleaf). +- **`view_region(bbox, [page_id])`** — crop a region of a page at full resolution (dense table, marginal note, faint ink). Omits `page_id` to zoom into the page in view. +- **`view_page(page_id)`** — load another full page from the same source. +- **`read_file(path)`** / **`list_dir([path])`** / **`grep(pattern, [path])`** — read and search the workspace (schemas, memory, prior outputs). Scoped to the workspace root. -The loop is capped (currently 8 tool calls per turn) so a confused expert can't spin. This means you don't have to predict the right crop up front — pass the page and let the expert zoom where it needs to. +So you don't have to predict the right crop up front — pass the page and let the expert zoom and cross-reference where it needs to. + +#### Granting elevated capabilities (off by default) + +Experts **cannot run commands or change files** unless the orchestrator passes `grant` on the `task`/`task_batch` call: + +- `grant: ["bash"]` — `bash(command)` (runs in the workspace dir) +- `grant: ["write"]` — `write_file(path, content)` +- `grant: ["edit"]` — `edit_file(path, old_text, new_text)` + +This path is deliberately gated for oversight and safety: requesting a grant triggers a **user confirmation** before any expert runs (once per `task` call, or once for a whole `task_batch` cohort), and denial aborts the call. Granted file operations are confined to the workspace. Whatever the expert does — every region viewed, file read/written, command run — is surfaced in the expert drawer (the "examined" steps; region/page steps are clickable, elevated actions are flagged), so the work stays auditable. Leave `grant` off unless a task genuinely needs the expert to act on its own. ### Bounding box cropping diff --git a/chronos-vscode/test/suite.js b/chronos-vscode/test/suite.js index 702889e..851b4bd 100644 --- a/chronos-vscode/test/suite.js +++ b/chronos-vscode/test/suite.js @@ -195,11 +195,16 @@ exports.run = async function run() { // 9. Expert oversight (#11): the task/task_batch drawer surfaces the expert's // own view_region/view_page calls as clickable viewer links. api.chronosTest.invoke("injectExpertTools"); - await waitFor("expert drawer shows clickable tool-use links", async () => { + await waitFor("expert drawer shows tool-use oversight", async () => { const s = await dump(); - return s?.chat?.expertOpen === "task-1" && s?.chat?.expertToolLinks === 2; + return ( + s?.chat?.expertOpen === "task-1" && + s?.chat?.expertToolLinks === 2 && // view_region + view_page → clickable links + s?.chat?.expertToolChips === 4 && // + grep + bash chips + s?.chat?.expertElevatedChips === 1 // bash flagged as elevated + ); }); - check("expert drawer surfaces tool-use viewer links", true); + check("expert drawer surfaces tool-use viewer links + flagged elevated actions", true); // Make sure the subprocess stayed alive throughout const after = api.getChronosStatus(); diff --git a/chronos-vscode/webview/components/chronos-chat.ts b/chronos-vscode/webview/components/chronos-chat.ts index 081097c..54f2047 100644 --- a/chronos-vscode/webview/components/chronos-chat.ts +++ b/chronos-vscode/webview/components/chronos-chat.ts @@ -53,6 +53,7 @@ interface ExpertToolUse { tool: string; pageId?: number; bbox?: { x: number; y: number; w: number; h: number }; + detail?: string; isError: boolean; } @@ -486,6 +487,8 @@ export class ChronosChat extends LitElement { running: boolean; expertOpen: string | null; expertToolLinks: number; + expertToolChips: number; + expertElevatedChips: number; } { const assistant = [...this.items].reverse().find((i) => i.kind === "assistant") as | { kind: "assistant"; message: AssistantMessage } @@ -497,8 +500,10 @@ export class ChronosChat extends LitElement { lastAssistant: assistant ? messageText(assistant.message.content) : "", running: this.running, expertOpen: this.openExpert, - // Clickable viewer links for the expert's own tool calls, in the open drawer. + // Oversight chips for the expert's own tool calls, in the open drawer. expertToolLinks: this.querySelectorAll(".expert-turn-tools a.view-link").length, + expertToolChips: this.querySelectorAll(".expert-turn-tools .expert-tool").length, + expertElevatedChips: this.querySelectorAll(".expert-turn-tools .expert-tool.is-elevated").length, }; } @@ -520,6 +525,8 @@ export class ChronosChat extends LitElement { toolUses: [ { tool: "view_region", pageId: 42, bbox: { x: 0.1, y: 0.3, w: 0.8, h: 0.1 }, isError: false }, { tool: "view_page", pageId: 43, isError: false }, + { tool: "grep", detail: '"Müller" in data', isError: false }, + { tool: "bash", detail: "wc -l data/entries.json", isError: false }, ], }, }, @@ -1029,21 +1036,38 @@ export class ChronosChat extends LitElement { `; } - // One expert tool call (view_region/view_page) as a clickable viewer link, so - // the historian can jump to exactly the page/region the expert pulled in. + // One expert tool call, for oversight. Page tools (view_region/view_page) are + // clickable viewer links so the historian can jump to exactly what the expert + // pulled in; file/search/command tools show as a labelled chip with the + // command/path/term. Elevated tools (bash/write/edit) are visually flagged. private renderExpertToolUse(u: ExpertToolUse): TemplateResult { - const label = u.tool === "view_region" ? "⛶ region" : u.tool === "view_page" ? "page" : u.tool.replace(/_/g, " "); - if (u.pageId == null) { - return html`${label}`; + if (u.pageId != null && (u.tool === "view_region" || u.tool === "view_page")) { + const label = u.tool === "view_region" ? "⛶ region" : "page"; + const bboxAttr = u.bbox ? `${u.bbox.x},${u.bbox.y},${u.bbox.w},${u.bbox.h}` : undefined; + return html`${label} p.${u.pageId}`; } - const bboxAttr = u.bbox ? `${u.bbox.x},${u.bbox.y},${u.bbox.w},${u.bbox.h}` : undefined; - return html`${label} p.${u.pageId} = { + read_file: "read", + list_dir: "ls", + grep: "grep", + bash: "⌘ bash", + write_file: "✎ write", + edit_file: "✎ edit", + }; + const elevated = u.tool === "bash" || u.tool === "write_file" || u.tool === "edit_file"; + const label = labels[u.tool] ?? u.tool.replace(/_/g, " "); + const detail = u.detail ? (u.detail.length > 64 ? u.detail.slice(0, 64) + "…" : u.detail) : ""; + return html`${label}${detail ? html`: ${detail}` : nothing}`; } diff --git a/chronos-vscode/webview/styles.css b/chronos-vscode/webview/styles.css index 906b7c2..9d40f5b 100644 --- a/chronos-vscode/webview/styles.css +++ b/chronos-vscode/webview/styles.css @@ -2162,6 +2162,13 @@ span.expert-tool { background: var(--ch-surface-2); } +/* Elevated (granted) actions stand out so they're easy to audit. */ +span.expert-tool.is-elevated { + color: var(--ch-warning); + background: color-mix(in srgb, var(--ch-warning) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--ch-warning) 30%, transparent); +} + .expert-tool.is-error { color: var(--ch-error); text-decoration: line-through; diff --git a/chronos/prompts/page-expert-prompt.md b/chronos/prompts/page-expert-prompt.md index 2ef4efd..18cb264 100644 --- a/chronos/prompts/page-expert-prompt.md +++ b/chronos/prompts/page-expert-prompt.md @@ -2,12 +2,20 @@ You are an expert analyst of historical German documents (18th-20th century). You are given a scanned page image. Answer the user's question about the page accurately and concisely. ## Tools -You can request more imagery before answering — use them when the full-page view is not enough: +You can act before answering — use these when the full-page view is not enough. By default you are +**read-only**: - **`view_region(bbox, [page_id])`** — zoom into a sub-region at full resolution to read dense tables, marginalia, or faint/damaged ink. `bbox` is normalized 0–1 (`x`/`y` = top-left, `w`/`h` = size). Omit `page_id` to zoom into the page you are currently looking at. - **`view_page(page_id)`** — load another full page from the same source (e.g. to follow a record that continues overleaf). `page_id` is the file-system index (1 = page_0001.png), not the printed number. +- **`read_file(path)`** — read a workspace text file (schemas, notes, memory, prior extractions). +- **`list_dir([path])`** — list a workspace directory. +- **`grep(pattern, [path])`** — search workspace text files for a regex/substring. + +Only if the orchestrator explicitly granted them (the user approved) will you also have: +- **`bash(command)`**, **`write_file(path, content)`**, **`edit_file(path, old_text, new_text)`** — run + commands / change files. Use these sparingly and only for what the task asked. Prefer zooming in over guessing. When you have enough detail, stop calling tools and give your answer. diff --git a/chronos/prompts/system-prompt.md b/chronos/prompts/system-prompt.md index e09dad1..ed9467a 100644 --- a/chronos/prompts/system-prompt.md +++ b/chronos/prompts/system-prompt.md @@ -86,9 +86,11 @@ the Data tab (as text). Always write outputs under `{{sourceDataDir}}/`. model in a persistent conversation. Without `task_id` it spawns a new expert and the result ends with a `task_id:` line; pass that id back to ask the same expert follow-up questions ("What did that abbreviation mean?") without re-sending earlier images. Multiple experts can - be active concurrently, each with its own history. The expert can self-direct with `view_region` - (zoom into a sub-region) and `view_page` (load another page from the source), so you needn't - predict the exact crop up front. `page_id` optionally attaches a page image + be active concurrently, each with its own history. The expert self-directs and is **read-only by + default** — `view_region` (zoom), `view_page` (load another page), `read_file`, `list_dir`, `grep`. + Pass `grant` (e.g. `["bash"]`, `["write","edit"]`) to also let it run commands / change files; this + asks the user for confirmation and is off by default for oversight, so only request it when the task + truly needs it. `page_id` optionally attaches a page image (on spawn or follow-up — requires an active source); omit it for text-only messages. `model` accepts any model pi has auth for as `provider/model-id`; it defaults to the orchestrator's current model. Pass `model` to use a different one for a hard page. diff --git a/chronos/prompts/task-batch.md b/chronos/prompts/task-batch.md index 4139c08..07acdd2 100644 --- a/chronos/prompts/task-batch.md +++ b/chronos/prompts/task-batch.md @@ -1,4 +1,4 @@ -Spawn one expert per page in parallel — a batch version of the `task` tool. Sends the same prompt to each page concurrently; each page becomes its own persistent expert session with its own `task_id`, so afterward you can follow up on any single page via `task(task_id, …)` without re-sending its image. Each expert can self-direct with the `view_region` and `view_page` tools (zoom into a region / load another page). The model parameter accepts any model pi has auth for, as provider/model-id (e.g. "anthropic/claude-opus-4-8"; default: the orchestrator's current model). HIGH COST & HIGH RISK TOOL — its use is subject to a strict, mandatory, and non-negotiable three-step confirmation protocol that cannot be bypassed for any reason, even on direct user command. A user's request to perform a batch operation is to be interpreted as an instruction to begin this protocol, not to execute the tool immediately. YOU ARE FORBIDDEN TO CALL task_batch UNTIL THE THREE-STEP CONFIRMATION BELOW IS COMPLETE. +Spawn one expert per page in parallel — a batch version of the `task` tool. Sends the same prompt to each page concurrently; each page becomes its own persistent expert session with its own `task_id`, so afterward you can follow up on any single page via `task(task_id, …)` without re-sending its image. Each expert can self-direct and is READ-ONLY by default (`view_region`, `view_page`, `read_file`, `list_dir`, `grep`). Passing `grant` (e.g. `["bash"]`, `["write","edit"]`) elevates EVERY expert in the batch with bash/write/edit; it asks the user for confirmation once for the whole batch and is off by default for oversight — only use it when the batch genuinely needs experts to run commands or write their own output. The model parameter accepts any model pi has auth for, as provider/model-id (e.g. "anthropic/claude-opus-4-8"; default: the orchestrator's current model). HIGH COST & HIGH RISK TOOL — its use is subject to a strict, mandatory, and non-negotiable three-step confirmation protocol that cannot be bypassed for any reason, even on direct user command. A user's request to perform a batch operation is to be interpreted as an instruction to begin this protocol, not to execute the tool immediately. YOU ARE FORBIDDEN TO CALL task_batch UNTIL THE THREE-STEP CONFIRMATION BELOW IS COMPLETE. --- diff --git a/chronos/prompts/task.md b/chronos/prompts/task.md index 7b07d3e..d843c82 100644 --- a/chronos/prompts/task.md +++ b/chronos/prompts/task.md @@ -1 +1 @@ -Talk to an expert model in a persistent conversation. Omit task_id to spawn a new expert: the result ends with a `task_id:` line — pass that id back to ask the same expert follow-up questions without re-sending earlier images (e.g. "What did that abbreviation mean?"). Multiple experts can be active at once, each with its own task_id and conversation history. The expert can also self-direct: it has `view_region` (zoom into a sub-region) and `view_page` (load another page from the source) tools, so you don't have to predict the exact crop up front — give it the page and let it zoom in where it needs to. page_id: optionally attach a page image (file-system index, e.g. 1 for page_0001.png — not the printed page number) on spawn or on any follow-up ("now compare with page 43"); omit it for a text-only message. prompt: what to ask. model: any model pi has auth for, as provider/model-id (default: the orchestrator's current model; an unknown model errors with the list of available models). On follow-ups the session's current model is reused unless you override it. output_file: if provided, write the response to this file in the source directory and return a short confirmation instead of the full text. When the output is structured records meant for the Data tab, prompt the expert to return a JSON array of row objects and to stamp each row with `chronos_page` (this page_id) — and optionally `chronos_bbox` as `[x,y,w,h]` normalized 0–1 — so the viewer can link each row back to its source region. A row that draws on several places may use lists for these keys (e.g. `chronos_page: [42, 43]`, or several `chronos_bbox` regions on one page); see "Structured data output" in the system prompt. +Talk to an expert model in a persistent conversation. Omit task_id to spawn a new expert: the result ends with a `task_id:` line — pass that id back to ask the same expert follow-up questions without re-sending earlier images (e.g. "What did that abbreviation mean?"). Multiple experts can be active at once, each with its own task_id and conversation history. The expert can also self-direct: by default it is READ-ONLY with `view_region` (zoom into a sub-region), `view_page` (load another page), `read_file`, `list_dir`, and `grep` over the workspace — so you don't have to predict the exact crop up front. It CANNOT run commands or change files unless you pass `grant` (e.g. `grant: ["bash"]` / `["write","edit"]`), which adds bash/write/edit for that one call. Granting requires the user's confirmation each time and is off by default to keep expert work auditable — only request it when the task genuinely needs the expert to run something or write output itself. page_id: optionally attach a page image (file-system index, e.g. 1 for page_0001.png — not the printed page number) on spawn or on any follow-up ("now compare with page 43"); omit it for a text-only message. prompt: what to ask. model: any model pi has auth for, as provider/model-id (default: the orchestrator's current model; an unknown model errors with the list of available models). On follow-ups the session's current model is reused unless you override it. output_file: if provided, write the response to this file in the source directory and return a short confirmation instead of the full text. When the output is structured records meant for the Data tab, prompt the expert to return a JSON array of row objects and to stamp each row with `chronos_page` (this page_id) — and optionally `chronos_bbox` as `[x,y,w,h]` normalized 0–1 — so the viewer can link each row back to its source region. A row that draws on several places may use lists for these keys (e.g. `chronos_page: [42, 43]`, or several `chronos_bbox` regions on one page); see "Structured data output" in the system prompt. diff --git a/chronos/tools/expert-tools.ts b/chronos/tools/expert-tools.ts index 9ce4d05..9a7fe3e 100644 --- a/chronos/tools/expert-tools.ts +++ b/chronos/tools/expert-tools.ts @@ -1,20 +1,36 @@ /** * Tools the vision-expert subagents (task / task_batch) can call during a turn. * - * Experts run an agentic loop in `runExpertTurn`: they receive the page image - * the orchestrator chose, then may self-direct — zoom into a sub-region they - * only identify after seeing the page (`view_region`), or pull in another page - * from the same source (`view_page`). Both reuse the existing crop/read paths, - * so a tool result is just another image block appended to the conversation. + * Experts run a bounded agentic loop in `runExpertTurn`. By default they are + * READ-ONLY — they can pull in more imagery and read the workspace, but cannot + * change anything or run commands. The orchestrator may elevate a specific + * expert by passing `grant` on the task/task_batch call (adding bash / write / + * edit), but that path is gated behind a human confirmation (see view-page.ts / + * task-batch.ts) and is off by default so expert work stays auditable. * * The `Tool` shape pi-ai consumes is only `{ name, description, parameters }` — * execution is the caller's job, handled by `executeExpertTool` below. */ +import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { resolve, relative, isAbsolute, dirname, join } from "node:path"; import { Type } from "@sinclair/typebox"; import type { ImageContent, TextContent, Tool, ToolCall, ToolResultMessage } from "@mariozechner/pi-ai"; import type { Bbox } from "../utils/crop-image.js"; import { pageImageContent } from "./expert-turn.js"; +const execAsync = promisify(exec); + +/** Capabilities the orchestrator can grant beyond read-only (human-gated). */ +export type ExpertCapability = "bash" | "write" | "edit"; + +const MAX_READ_CHARS = 100_000; +const MAX_GREP_MATCHES = 100; +const MAX_GREP_FILES = 3000; +const BASH_TIMEOUT_MS = 30_000; +const SKIP_DIRS = new Set(["node_modules", ".git", ".chronos", "png", "dist", "out"]); + const bboxSchema = Type.Object({ x: Type.Number({ minimum: 0, maximum: 1 }), y: Type.Number({ minimum: 0, maximum: 1 }), @@ -22,35 +38,123 @@ const bboxSchema = Type.Object({ h: Type.Number({ minimum: 0, maximum: 1 }), }); -/** Tool definitions handed to `complete()` so the expert can request more imagery. */ -export const EXPERT_TOOLS: Tool[] = [ - { - name: "view_region", - description: - "Zoom into a sub-region of a page at full resolution. Use this to read dense tables, " + - "marginalia, or faint/damaged ink that is too small in the full-page view. bbox is the " + - "crop in normalized coordinates (0–1): x/y is the top-left corner, w/h is width/height. " + - "page_id is optional — omit it to zoom into the page you are currently looking at.", - parameters: Type.Object({ - bbox: bboxSchema, - page_id: Type.Optional( - Type.Number({ description: "Page to crop (file-system index). Omit to use the current page." }), - ), - }), - }, - { - name: "view_page", - description: - "Load another full page from the same source into the conversation — e.g. to compare with a " + - "neighbouring page or follow a record that continues overleaf. page_id is the file-system index " + - "(1 = page_0001.png), not the printed page number.", - parameters: Type.Object({ - page_id: Type.Number({ description: "Page to load (file-system index)." }), - }), - }, -]; - -/** Provenance of an image returned by a tool, so persistence can re-crop it from disk on restore. */ +// ── Read-only tools (always available) ────────────────────────────────────── + +const VIEW_REGION_TOOL: Tool = { + name: "view_region", + description: + "Zoom into a sub-region of a page at full resolution. Use this to read dense tables, " + + "marginalia, or faint/damaged ink that is too small in the full-page view. bbox is the " + + "crop in normalized coordinates (0–1): x/y is the top-left corner, w/h is width/height. " + + "page_id is optional — omit it to zoom into the page you are currently looking at.", + parameters: Type.Object({ + bbox: bboxSchema, + page_id: Type.Optional( + Type.Number({ description: "Page to crop (file-system index). Omit to use the current page." }), + ), + }), +}; + +const VIEW_PAGE_TOOL: Tool = { + name: "view_page", + description: + "Load another full page from the same source into the conversation — e.g. to compare with a " + + "neighbouring page or follow a record that continues overleaf. page_id is the file-system index " + + "(1 = page_0001.png), not the printed page number.", + parameters: Type.Object({ + page_id: Type.Number({ description: "Page to load (file-system index)." }), + }), +}; + +const READ_FILE_TOOL: Tool = { + name: "read_file", + description: + "Read a UTF-8 text file from the workspace (read-only). Use for schemas, notes, memory, or " + + "previously-extracted data. path is relative to the workspace root; it must stay inside the workspace.", + parameters: Type.Object({ path: Type.String({ description: "Workspace-relative file path." }) }), +}; + +const LIST_DIR_TOOL: Tool = { + name: "list_dir", + description: + "List the entries of a workspace directory (read-only). path is relative to the workspace root " + + "(omit for the root). Directories are suffixed with '/'.", + parameters: Type.Object({ path: Type.Optional(Type.String({ description: "Workspace-relative directory." })) }), +}; + +const GREP_TOOL: Tool = { + name: "grep", + description: + "Search workspace text files for a pattern (read-only). pattern is a regular expression (falls back " + + "to a literal substring if it isn't valid regex). Returns up to 100 matches as path:line: text. " + + "Optional path narrows the search to a subdirectory. Skips binaries and bulky dirs (png/, node_modules/, …).", + parameters: Type.Object({ + pattern: Type.String({ description: "Regex or substring to search for." }), + path: Type.Optional(Type.String({ description: "Workspace-relative subdirectory to search (default: whole workspace)." })), + }), +}; + +// ── Elevated tools (only added when granted + human-approved) ──────────────── + +const BASH_TOOL: Tool = { + name: "bash", + description: + "Run a shell command in the workspace directory and return its output. Only available when the " + + "orchestrator granted this expert the \"bash\" capability (which the user approved).", + parameters: Type.Object({ command: Type.String({ description: "Shell command to run." }) }), +}; + +const WRITE_FILE_TOOL: Tool = { + name: "write_file", + description: + "Create or overwrite a workspace text file. Only available when the orchestrator granted the " + + "\"write\" capability (user-approved). path is workspace-relative and must stay inside the workspace.", + parameters: Type.Object({ + path: Type.String({ description: "Workspace-relative file path." }), + content: Type.String({ description: "Full file contents." }), + }), +}; + +const EDIT_FILE_TOOL: Tool = { + name: "edit_file", + description: + "Replace text in a workspace file. Only available when the orchestrator granted the \"edit\" " + + "capability (user-approved). old_text must occur in the file; every occurrence is replaced.", + parameters: Type.Object({ + path: Type.String({ description: "Workspace-relative file path." }), + old_text: Type.String({ description: "Exact text to replace (must be present)." }), + new_text: Type.String({ description: "Replacement text." }), + }), +}; + +const ELEVATED_TOOLS: Record = { + bash: BASH_TOOL, + write: WRITE_FILE_TOOL, + edit: EDIT_FILE_TOOL, +}; + +const ELEVATED_TOOL_NAMES: Record = { + bash: "bash", + write_file: "write", + edit_file: "edit", +}; + +/** + * The tools an expert sees this turn: read-only file/search tools always, the + * image tools only when the model can consume images, and the elevated tools + * only for capabilities the orchestrator granted (and the user approved). + */ +export function buildExpertTools(opts: { vision: boolean; granted: ExpertCapability[] }): Tool[] { + const tools: Tool[] = []; + if (opts.vision) tools.push(VIEW_REGION_TOOL, VIEW_PAGE_TOOL); + tools.push(READ_FILE_TOOL, LIST_DIR_TOOL, GREP_TOOL); + for (const cap of opts.granted) { + const tool = ELEVATED_TOOLS[cap]; + if (tool && !tools.includes(tool)) tools.push(tool); + } + return tools; +} + export interface ExpertToolImageRef { pageId: number; bbox?: Bbox; @@ -63,9 +167,20 @@ export interface PersistedToolResult { toolName: string; isError: boolean; text: string; + /** Short human label for oversight (command run, file path, …). */ + detail?: string; image?: ExpertToolImageRef; } +export interface ExpertToolContext { + sourceDir: string | undefined; + currentPageId: number | null; + /** Workspace root — read/search/write/edit/bash are scoped to it. */ + cwd: string; + /** Capabilities the orchestrator granted (and the user approved) for this expert. */ + granted: ReadonlySet; +} + export interface ExpertToolOutcome { message: ToolResultMessage; persist: PersistedToolResult; @@ -82,65 +197,216 @@ function coerceBbox(value: unknown): Bbox | null { return null; } +// Resolve a workspace-relative path and refuse anything that escapes the root. +function resolveInWorkspace(cwd: string, p: string): string { + const abs = resolve(cwd, p); + const rel = relative(cwd, abs); + if (rel === "" ) return abs; + if (rel.startsWith("..") || isAbsolute(rel)) { + throw new Error("Path is outside the workspace."); + } + return abs; +} + +function* walkFiles(dir: string, depth = 0): Generator { + if (depth > 12) return; + let entries: import("node:fs").Dirent[]; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + if (e.isDirectory()) { + if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue; + yield* walkFiles(join(dir, e.name), depth + 1); + } else if (e.isFile()) { + yield join(dir, e.name); + } + } +} + /** - * Execute one expert tool call against the current source. Never throws: a bad - * bbox / missing page / absent source comes back as an `isError` tool result so - * the loop can feed it to the model and let it recover (and count toward the cap). + * Execute one expert tool call. Never throws: bad args / missing files / denied + * capabilities come back as `isError` tool results so the loop can feed them to + * the model and let it recover (and they count toward the iteration cap). */ -export async function executeExpertTool( - call: ToolCall, - ctx: { sourceDir: string | undefined; currentPageId: number | null }, -): Promise { - const base = { - role: "toolResult" as const, - toolCallId: call.id, - toolName: call.name, - timestamp: Date.now(), - }; - const fail = (text: string): ExpertToolOutcome => ({ +export async function executeExpertTool(call: ToolCall, ctx: ExpertToolContext): Promise { + const base = { role: "toolResult" as const, toolCallId: call.id, toolName: call.name, timestamp: Date.now() }; + const fail = (text: string, detail?: string): ExpertToolOutcome => ({ message: { ...base, content: [{ type: "text", text }], isError: true }, - persist: { toolCallId: call.id, toolName: call.name, isError: true, text }, + persist: { toolCallId: call.id, toolName: call.name, isError: true, text, detail }, + }); + const text = (body: string, detail?: string, isError = false): ExpertToolOutcome => ({ + message: { ...base, content: [{ type: "text", text: body }], isError }, + persist: { toolCallId: call.id, toolName: call.name, isError, text: body, detail }, }); - if (!ctx.sourceDir) return fail("No source is active, so page imagery cannot be loaded."); + // Defense in depth: never run an elevated tool the orchestrator didn't grant. + const requiredCap = ELEVATED_TOOL_NAMES[call.name]; + if (requiredCap && !ctx.granted.has(requiredCap)) { + return fail(`The "${call.name}" tool is not permitted (the "${requiredCap}" capability was not granted to this expert).`); + } + + const args = (call.arguments ?? {}) as Record; + + switch (call.name) { + case "view_page": + case "view_region": { + if (!ctx.sourceDir) return fail("No source is active, so page imagery cannot be loaded."); + const isRegion = call.name === "view_region"; + let bbox: Bbox | undefined; + if (isRegion) { + const parsed = coerceBbox(args.bbox); + if (!parsed) return fail("view_region requires bbox as { x, y, w, h }, each normalized 0–1."); + bbox = parsed; + } + const rawPage = args.page_id; + const pageId = rawPage !== undefined && rawPage !== null ? Math.round(Number(rawPage)) : ctx.currentPageId; + if (pageId === null || !Number.isFinite(pageId)) { + return fail(`${call.name} needs a page_id (no page is currently in view).`); + } + try { + const image: ImageContent = await pageImageContent(ctx.sourceDir, pageId, bbox); + const note = isRegion ? `Zoomed into the requested region of page ${pageId}.` : `Loaded page ${pageId}.`; + const content: (ImageContent | TextContent)[] = [image, { type: "text", text: note }]; + return { + message: { ...base, content, isError: false }, + persist: { + toolCallId: call.id, + toolName: call.name, + isError: false, + text: note, + image: { pageId, bbox, sourceDir: ctx.sourceDir }, + }, + viewedPageId: pageId, + }; + } catch (e) { + return fail((e as Error).message); + } + } - if (call.name === "view_page" || call.name === "view_region") { - const isRegion = call.name === "view_region"; - let bbox: Bbox | undefined; - if (isRegion) { - const parsed = coerceBbox(call.arguments?.bbox); - if (!parsed) return fail("view_region requires bbox as { x, y, w, h }, each normalized 0–1."); - bbox = parsed; + case "read_file": { + const p = String(args.path ?? ""); + if (!p) return fail("read_file requires a path."); + try { + const abs = resolveInWorkspace(ctx.cwd, p); + let body = readFileSync(abs, "utf-8"); + if (body.length > MAX_READ_CHARS) body = body.slice(0, MAX_READ_CHARS) + "\n…[truncated]"; + return text(body || "(empty file)", p); + } catch (e) { + return fail(`read_file failed: ${(e as Error).message}`, p); + } } - const rawPage = call.arguments?.page_id; - const pageId = - rawPage !== undefined && rawPage !== null ? Math.round(Number(rawPage)) : ctx.currentPageId; - if (pageId === null || !Number.isFinite(pageId)) { - return fail(`${call.name} needs a page_id (no page is currently in view).`); + + case "list_dir": { + const p = String(args.path ?? ""); + try { + const abs = p ? resolveInWorkspace(ctx.cwd, p) : ctx.cwd; + const entries = readdirSync(abs, { withFileTypes: true }) + .map((e) => (e.isDirectory() ? `${e.name}/` : e.name)) + .sort(); + return text(entries.join("\n") || "(empty directory)", p || "."); + } catch (e) { + return fail(`list_dir failed: ${(e as Error).message}`, p || "."); + } } - try { - const image: ImageContent = await pageImageContent(ctx.sourceDir, pageId, bbox); - const text = isRegion - ? `Zoomed into the requested region of page ${pageId}.` - : `Loaded page ${pageId}.`; - const content: (ImageContent | TextContent)[] = [image, { type: "text", text }]; - return { - message: { ...base, content, isError: false }, - persist: { - toolCallId: call.id, - toolName: call.name, - isError: false, - text, - image: { pageId, bbox, sourceDir: ctx.sourceDir }, - }, - viewedPageId: pageId, - }; - } catch (e) { - return fail((e as Error).message); + + case "grep": { + const pattern = String(args.pattern ?? ""); + if (!pattern) return fail("grep requires a pattern."); + const sub = String(args.path ?? ""); + let root: string; + try { + root = sub ? resolveInWorkspace(ctx.cwd, sub) : ctx.cwd; + } catch (e) { + return fail(`grep failed: ${(e as Error).message}`, pattern); + } + let re: RegExp; + try { + re = new RegExp(pattern); + } catch { + re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + } + const matches: string[] = []; + let scanned = 0; + outer: for (const file of walkFiles(root)) { + if (++scanned > MAX_GREP_FILES) break; + let content: string; + try { + if (statSync(file).size > 2_000_000) continue; + content = readFileSync(file, "utf-8"); + } catch { + continue; + } + if (content.includes("\u0000")) continue; // binary + const rel = relative(ctx.cwd, file); + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (re.test(lines[i])) { + matches.push(`${rel}:${i + 1}: ${lines[i].trim().slice(0, 200)}`); + if (matches.length >= MAX_GREP_MATCHES) break outer; + } + } + } + const body = matches.length ? matches.join("\n") : "(no matches)"; + return text(body, `"${pattern}"${sub ? ` in ${sub}` : ""}`); + } + + case "bash": { + const command = String(args.command ?? ""); + if (!command) return fail("bash requires a command.", command); + try { + const { stdout, stderr } = await execAsync(command, { + cwd: ctx.cwd, + timeout: BASH_TIMEOUT_MS, + maxBuffer: 1024 * 1024, + encoding: "utf-8", + }); + const out = [stdout, stderr].filter(Boolean).join("\n").trim(); + return text(out || "(no output)", command); + } catch (e) { + const err = e as { stdout?: string; stderr?: string; message: string }; + const out = [err.stdout, err.stderr, err.message].filter(Boolean).join("\n").trim(); + return text(out || "(command failed)", command, true); + } + } + + case "write_file": { + const p = String(args.path ?? ""); + const content = typeof args.content === "string" ? args.content : ""; + if (!p) return fail("write_file requires a path."); + try { + const abs = resolveInWorkspace(ctx.cwd, p); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, content, "utf-8"); + return text(`Wrote ${p} (${content.length} chars).`, p); + } catch (e) { + return fail(`write_file failed: ${(e as Error).message}`, p); + } } - } - return fail(`Unknown tool "${call.name}".`); + case "edit_file": { + const p = String(args.path ?? ""); + const oldText = String(args.old_text ?? ""); + const newText = typeof args.new_text === "string" ? args.new_text : ""; + if (!p) return fail("edit_file requires a path."); + if (!oldText) return fail("edit_file requires old_text.", p); + try { + const abs = resolveInWorkspace(ctx.cwd, p); + const before = readFileSync(abs, "utf-8"); + if (!before.includes(oldText)) return fail(`edit_file: old_text not found in ${p}.`, p); + const count = before.split(oldText).length - 1; + writeFileSync(abs, before.split(oldText).join(newText), "utf-8"); + return text(`Edited ${p} (${count} replacement${count === 1 ? "" : "s"}).`, p); + } catch (e) { + return fail(`edit_file failed: ${(e as Error).message}`, p); + } + } + + default: + return fail(`Unknown tool "${call.name}".`); + } } /** Rebuild a persisted tool result on session restore, re-cropping its image from disk. */ diff --git a/chronos/tools/expert-turn.ts b/chronos/tools/expert-turn.ts index dc27f7d..42476cd 100644 --- a/chronos/tools/expert-turn.ts +++ b/chronos/tools/expert-turn.ts @@ -16,11 +16,44 @@ import { requireSource } from "./source-context.js"; import { resolveExpertModel } from "../utils/resolve-model.js"; import { cropImageToBase64, type Bbox } from "../utils/crop-image.js"; import { appendExpertTurn, type PersistedExpert, type PersistedStep } from "../utils/expert-store.js"; -import { EXPERT_TOOLS, executeExpertTool, rehydrateToolResult } from "./expert-tools.js"; +import { + buildExpertTools, + executeExpertTool, + rehydrateToolResult, + type ExpertCapability, +} from "./expert-tools.js"; // Bound the per-turn agentic loop so a confused expert can't spin on tool calls. const MAX_EXPERT_TOOL_CALLS = 8; +const CAP_DESCRIPTION: Record = { + bash: "run shell commands", + write: "create files", + edit: "modify files", +}; + +/** + * Ask the user to approve elevating an expert beyond read-only. Experts are + * read-only by default so their work stays auditable; bash/write/edit are off + * unless the orchestrator requests them AND the user approves here. Always + * prompts (it is the oversight gate). Returns true if approved; `scope` + * describes who gets the grant (e.g. "this expert", "all 12 experts in this batch"). + */ +export async function confirmExpertGrant( + extCtx: ExtensionContext, + caps: ExpertCapability[], + scope: string, +): Promise { + if (caps.length === 0) return true; + const list = caps.map((c) => `${c} (${CAP_DESCRIPTION[c]})`).join(", "); + return extCtx.ui.confirm( + "Grant expert elevated access?", + `The agent wants to let ${scope} go beyond read-only — ${list} — in the workspace. ` + + `Expert subagents are read-only by default so their work stays auditable; this is normally ` + + `disabled for oversight and safety. Allow for this call?`, + ); +} + export function modelSpec(m: { provider: string; id: string }): string { return `${m.provider}/${m.id}`; } @@ -50,6 +83,12 @@ export interface ExpertTurnInput { bbox?: Bbox; /** Abort the (multi-call) agentic loop when the user cancels. */ signal?: AbortSignal; + /** + * Elevated capabilities the orchestrator granted this expert (bash/write/edit). + * Read-only tools are always available; these are added only when present, and + * the caller is responsible for getting the user's confirmation first. + */ + grantedCaps?: ExpertCapability[]; } /** One tool the expert invoked during a turn — surfaced to the UI for oversight. */ @@ -57,6 +96,8 @@ export interface ExpertToolUse { tool: string; pageId?: number; bbox?: Bbox; + /** Short label for non-page tools (command run, file path, search term). */ + detail?: string; isError: boolean; } @@ -158,9 +199,15 @@ export async function runExpertTurn( const steps: PersistedStep[] = []; let currentPageId: number | null = pageId; let toolCallCount = 0; - // Only offer the image-returning view tools to a model that can actually - // consume images; a text-only model just answers in text. - let toolsEnabled = resolved.model.input.includes("image"); + // Read-only tools are always offered; the image tools only when the model can + // consume images; bash/write/edit only for capabilities the orchestrator + // granted (and the user approved upstream). + const granted = new Set(input.grantedCaps ?? []); + const expertToolDefs = buildExpertTools({ + vision: resolved.model.input.includes("image"), + granted: [...granted], + }); + let toolsEnabled = expertToolDefs.length > 0; let totalCost = 0; let finalResponse; @@ -173,7 +220,7 @@ export async function runExpertTurn( { systemPrompt: pageExpertPrompt, messages: [...session.messages, ...turnMessages], - tools: toolsEnabled ? EXPERT_TOOLS : undefined, + tools: toolsEnabled ? expertToolDefs : undefined, }, { apiKey: resolved.apiKey, headers: resolved.headers, signal: input.signal }, ); @@ -195,7 +242,12 @@ export async function runExpertTurn( steps.push({ kind: "assistant", message: response }); for (const call of toolCalls) { toolCallCount++; - const outcome = await executeExpertTool(call, { sourceDir: turnSourceDir, currentPageId }); + const outcome = await executeExpertTool(call, { + sourceDir: turnSourceDir, + currentPageId, + cwd: extCtx.cwd, + granted, + }); turnMessages.push(outcome.message); steps.push({ kind: "toolResult", toolResult: outcome.persist }); if (outcome.viewedPageId !== undefined) currentPageId = outcome.viewedPageId; @@ -230,6 +282,7 @@ export async function runExpertTurn( tool: s.toolResult.toolName, pageId: s.toolResult.image?.pageId, bbox: s.toolResult.image?.bbox, + detail: s.toolResult.detail, isError: s.toolResult.isError, })); diff --git a/chronos/tools/task-batch.ts b/chronos/tools/task-batch.ts index 96115c9..523d1ef 100644 --- a/chronos/tools/task-batch.ts +++ b/chronos/tools/task-batch.ts @@ -6,7 +6,8 @@ import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; import type { ToolText } from "../utils/tool-loader.js"; -import { runExpertTurn, type ExpertTurnInput, type ExpertToolUse } from "./expert-turn.js"; +import { runExpertTurn, confirmExpertGrant, type ExpertTurnInput, type ExpertToolUse } from "./expert-turn.js"; +import type { ExpertCapability } from "./expert-tools.js"; import { type Bbox } from "../utils/crop-image.js"; interface ExpertEntry { @@ -62,6 +63,15 @@ const taskBatchParams = Type.Object({ { description: "Crop region in normalized coordinates (0–1). Applied to every page before sending to the model." } ) ), + grant: Type.Optional( + Type.Array(Type.Union([Type.Literal("bash"), Type.Literal("write"), Type.Literal("edit")]), { + description: + 'Elevate EVERY expert in this batch beyond read-only: "bash" (run shell commands), "write" ' + + '(create files), "edit" (modify files). REQUIRES the user\'s confirmation (asked once for the whole ' + + "batch) — experts are read-only by default so their work stays auditable; normally disabled for " + + "oversight and safety. Omit for normal read-only experts.", + }), + ), }); export function createTaskBatchTool( @@ -93,11 +103,28 @@ export function createTaskBatchTool( return { content: [{ type: "text", text: "No page IDs provided." }], details: {} }; } + // Elevated capabilities are confirmed ONCE for the whole cohort, before any + // expert runs. Denial aborts the batch. + const grant: ExpertCapability[] = params.grant ?? []; + if (grant.length > 0 && !(await confirmExpertGrant(extCtx, grant, `all ${pageIds.length} experts in this batch`))) { + return { + content: [ + { + type: "text", + text: + `User declined to grant elevated access (${grant.join(", ")}); no experts were run. ` + + "Re-issue without `grant` to run read-only, or ask the user to approve.", + }, + ], + details: {}, + }; + } + const experts: ExpertEntry[] = []; let resolvedModel = params.model ?? "(orchestrator default)"; const runOne = async (pageId: number): Promise => { - const input: ExpertTurnInput = { prompt: params.prompt, model: params.model, pageId, bbox, signal }; + const input: ExpertTurnInput = { prompt: params.prompt, model: params.model, pageId, bbox, signal, grantedCaps: grant }; const result = await runExpertTurn(registry, sourceCtx, pageExpertPrompt, extCtx, input); if (!result.ok) { return { page_id: pageId, status: "error", error: result.error }; diff --git a/chronos/tools/view-page.ts b/chronos/tools/view-page.ts index fcee527..cf7d69a 100644 --- a/chronos/tools/view-page.ts +++ b/chronos/tools/view-page.ts @@ -5,7 +5,18 @@ import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; -import { runExpertTurn } from "./expert-turn.js"; +import { runExpertTurn, confirmExpertGrant } from "./expert-turn.js"; +import type { ExpertCapability } from "./expert-tools.js"; + +const grantParam = Type.Optional( + Type.Array(Type.Union([Type.Literal("bash"), Type.Literal("write"), Type.Literal("edit")]), { + description: + 'Elevate this expert beyond read-only by granting capabilities: "bash" (run shell commands), ' + + '"write" (create files), "edit" (modify files). REQUIRES the user\'s confirmation each time — ' + + "experts are read-only by default (they can view pages and read the workspace) so their work stays " + + "auditable; this is normally disabled for oversight and safety. Omit for a normal read-only expert.", + }), +); const taskParams = Type.Object({ prompt: Type.String({ description: "What to ask the expert model." }), @@ -49,6 +60,7 @@ const taskParams = Type.Object({ { description: "Crop region in normalized coordinates (0–1). x/y is the top-left corner, w/h is width/height. Crops the image before sending. Requires page_id." } ) ), + grant: grantParam, }); export function createTaskTool( @@ -63,6 +75,20 @@ export function createTaskTool( description, parameters: taskParams, async execute(_toolCallId, params, signal, _onUpdate, extCtx) { + const grant: ExpertCapability[] = params.grant ?? []; + if (grant.length > 0 && !(await confirmExpertGrant(extCtx, grant, "this expert"))) { + return { + content: [ + { + type: "text", + text: + `User declined to grant elevated access (${grant.join(", ")}); the expert was not run. ` + + "Re-issue without `grant` to run read-only, or ask the user to approve.", + }, + ], + details: {}, + }; + } const result = await runExpertTurn(registry, sourceCtx, pageExpertPrompt, extCtx, { taskId: params.task_id, prompt: params.prompt, @@ -70,6 +96,7 @@ export function createTaskTool( pageId: params.page_id, bbox: params.bbox, signal, + grantedCaps: grant, }); if (!result.ok) { From cc1d8c8f70d8e51ca34f5cbd997e290f7e01ff04 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 12:53:13 +0200 Subject: [PATCH 09/17] feat(extension): dedicated setup command + Getting Started walkthrough Split machine-level dependency install out of "Init Workspace" into its own chronos.setup command, and add a Getting Started walkthrough so onboarding is discoverable and each step's scope is distinct. - chronos.setup ("Install Dependencies") wraps ensureBootstrap; chronos.init no longer installs deps (it only scaffolds the workspace); startSession keeps its lazy gate. - Install runs as an awaited VS Code task (runSetupTask) instead of a fire-and-forget terminal, so the flow auto-continues instead of telling the user to re-run the command. - Getting Started walkthrough (4 steps) with context-key-driven completion; auto-opens once on first run when pi is missing. Activation moved to onStartupFinished; HttpServer now binds lazily so startup has no side effects. - Unify pi-binary detection with the launcher: extract resolvePiBin + hasPi into pi-env.ts so "is pi present?" matches "how pi is launched" (fixes false negatives when pi isn't on GUI VS Code's PATH). - Dev overrides: chronos.piPackageSource (install the pi-package from a local path or branch) and chronos.piNpmPackage; default pi CLI package is the maintained @earendil-works/pi-coding-agent. Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/package.json | 72 +++++++- chronos-vscode/src/extension.ts | 193 +++++++++++++++++----- chronos-vscode/src/http-server.ts | 19 ++- chronos-vscode/src/panel/chronos-panel.ts | 28 +--- chronos-vscode/src/pi-env.ts | 44 +++++ chronos-vscode/walkthroughs/init.md | 19 +++ chronos-vscode/walkthroughs/login.md | 16 ++ chronos-vscode/walkthroughs/setup.md | 19 +++ chronos-vscode/walkthroughs/start.md | 14 ++ 9 files changed, 348 insertions(+), 76 deletions(-) create mode 100644 chronos-vscode/src/pi-env.ts create mode 100644 chronos-vscode/walkthroughs/init.md create mode 100644 chronos-vscode/walkthroughs/login.md create mode 100644 chronos-vscode/walkthroughs/setup.md create mode 100644 chronos-vscode/walkthroughs/start.md diff --git a/chronos-vscode/package.json b/chronos-vscode/package.json index 2646142..c29b11f 100644 --- a/chronos-vscode/package.json +++ b/chronos-vscode/package.json @@ -17,7 +17,7 @@ "Other" ], "activationEvents": [ - "onCommand:chronos.startSession" + "onStartupFinished" ], "main": "./out/extension.js", "contributes": { @@ -30,6 +30,10 @@ "command": "chronos.showPage", "title": "Chronos: Show Page" }, + { + "command": "chronos.setup", + "title": "Chronos: Install Dependencies (pi + agent)" + }, { "command": "chronos.init", "title": "Chronos: Init Workspace" @@ -54,9 +58,73 @@ "type": "string", "default": "", "description": "Path to the pi binary. Leave empty to use \"pi\" from PATH." + }, + "chronos.piPackageSource": { + "type": "string", + "default": "", + "markdownDescription": "**Dev override.** Source the setup flow installs the Chronos pi-package from. Accepts a local path (`/abs/path/to/chronos/chronos`), a git URL, or a git URL with a branch/tag using the `@` form (`https://github.com/ai-historian/chronos@my-branch`). Leave empty to use the GitHub release pinned to this extension's version." + }, + "chronos.piNpmPackage": { + "type": "string", + "default": "", + "markdownDescription": "**Dev override.** npm package the setup flow installs for the `pi` CLI when pi isn't found. Leave empty for the default `@earendil-works/pi-coding-agent` (the maintained package). Set to override it — e.g. a fork or a version-pinned tag like `@earendil-works/pi-coding-agent@0.79.10`." } } - } + }, + "walkthroughs": [ + { + "id": "chronos.gettingStarted", + "title": "Get Started with Chronos", + "description": "Install the agent, scaffold a workspace, connect an AI provider, and start digitizing historical sources.", + "steps": [ + { + "id": "chronos.setup", + "title": "Install dependencies", + "description": "Chronos runs on the `pi` agent CLI plus the Chronos pi-package — installed once per machine.\n[Install dependencies](command:chronos.setup)", + "media": { + "markdown": "walkthroughs/setup.md" + }, + "completionEvents": [ + "onContext:chronos.dependenciesInstalled" + ] + }, + { + "id": "chronos.init", + "title": "Initialize a workspace", + "description": "Open a folder, then scaffold it into a Chronos workspace (sources/, data/, memory/, …).\n[Init workspace](command:chronos.init)", + "media": { + "markdown": "walkthroughs/init.md" + }, + "completionEvents": [ + "onContext:chronos.workspaceInitialized" + ] + }, + { + "id": "chronos.startSession", + "title": "Start an agent session", + "description": "Open the Chronos panel — page viewer plus chat — to begin working.\n[Start session](command:chronos.startSession)", + "media": { + "markdown": "walkthroughs/start.md" + }, + "completionEvents": [ + "onCommand:chronos.startSession" + ] + }, + { + "id": "chronos.login", + "title": "Connect an AI provider", + "description": "Store an API key (Anthropic, Google, OpenAI, …) the agent will use. Use the **Log in** button in the panel header, or run the command.\n[Connect provider](command:chronos.login)", + "media": { + "markdown": "walkthroughs/login.md" + }, + "completionEvents": [ + "onContext:chronos.providerConnected", + "onCommand:chronos.login" + ] + } + ] + } + ] }, "scripts": { "vscode:prepublish": "npm run build", diff --git a/chronos-vscode/src/extension.ts b/chronos-vscode/src/extension.ts index 12d6b4e..d50d0d8 100644 --- a/chronos-vscode/src/extension.ts +++ b/chronos-vscode/src/extension.ts @@ -3,7 +3,7 @@ import * as path from "path"; import { readdirSync, statSync, existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, rmSync, renameSync } from "node:fs"; import { join, relative, extname, basename } from "node:path"; import { Worker } from "node:worker_threads"; -import { execSync } from "node:child_process"; +import { hasPi } from "./pi-env"; // mupdf is ESM-only — loaded via dynamic import() where needed import { HttpServer } from "./http-server"; import { ChronosPanel } from "./panel/chronos-panel"; @@ -21,22 +21,16 @@ import { findIncompleteImports, } from "./import-status"; -const PI_NPM_PACKAGE = "@mariozechner/pi-coding-agent"; +// pi's npm package. @mariozechner/pi-coding-agent is deprecated and frozen at +// 0.73.1 ("use @earendil-works/pi-coding-agent going forward"); the renamed +// package is the maintained one. Override per-machine via chronos.piNpmPackage. +const PI_NPM_PACKAGE = "@earendil-works/pi-coding-agent"; const CHRONOS_PI_PACKAGE = "https://github.com/ai-historian/chronos"; // The pi-package repo was renamed history-agent → chronos. GitHub redirects the // old URL, but pi keys a package's identity on the literal host/path, so an // existing history-agent entry lingers as a duplicate unless we migrate it. const LEGACY_PI_PACKAGE = "https://github.com/ai-historian/history-agent"; -function hasPi(): boolean { - try { - execSync("pi --version", { stdio: "ignore" }); - return true; - } catch { - return false; - } -} - // pi's user settings list the configured packages (each entry is either a // source string or a { source } object). Read them so we can detect what's // installed instead of asking the user a question pi can already answer. @@ -83,6 +77,42 @@ function hasLocalChronosCheckout(): boolean { return piPackageSources().some((s) => isChronosEntry(s) && !isGitUrlEntry(s)); } +// Run a shell command as a VS Code task and resolve with its exit code. A task +// runs in the user's shell — login-shell PATH, sudo prompts, visible output, just +// like the integrated terminal — but unlike terminal.sendText it reports +// completion, so the caller can await the install and continue automatically +// instead of asking the user to re-run the command. Never rejects: a failure to +// launch resolves to -1 so callers handle it uniformly via the exit code. +function runSetupTask(name: string, command: string): Promise { + const task = new vscode.Task( + { type: "chronos-setup" }, + vscode.TaskScope.Workspace, + name, + "Chronos", + new vscode.ShellExecution(command), + ); + task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Dedicated, + clear: true, + focus: false, + echo: true, + }; + return new Promise((resolve) => { + // Subscribe before executing so a fast-failing task can't end before we listen. + const sub = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution.task === task) { + sub.dispose(); + resolve(e.exitCode ?? -1); + } + }); + vscode.tasks.executeTask(task).then(undefined, () => { + sub.dispose(); + resolve(-1); + }); + }); +} + async function ensureBootstrap(context: vscode.ExtensionContext): Promise { // Integration tests drive a mock pi binary and have no real pi/package to // install — skip the (modal, test-refused) bootstrap prompts. @@ -91,47 +121,64 @@ async function ensureBootstrap(context: vscode.ExtensionContext): Promise` tag on the repo per release. + // + // Dev overrides: chronos.piPackageSource installs the pi-package from a local + // path or a branch (".../chronos/chronos" or "@my-branch") instead of the + // pinned release; chronos.piNpmPackage swaps the npm package for the pi CLI + // itself (e.g. a fork). Both fall back to the release behaviour when unset. + const cfg = vscode.workspace.getConfiguration("chronos"); + const sourceOverride = cfg.get("piPackageSource")?.trim(); + const npmPackage = cfg.get("piNpmPackage")?.trim() || PI_NPM_PACKAGE; const wantedRef = `v${context.extension.packageJSON.version}`; // Pin with `@`, NOT `#`: pi's URL parser mishandles the `#` fragment // form and passes it straight to `git clone` (which fails). The `@` form is // split into a clean repo URL + a checkout ref. - const wantedSource = `${CHRONOS_PI_PACKAGE}@${wantedRef}`; + const wantedSource = sourceOverride || `${CHRONOS_PI_PACKAGE}@${wantedRef}`; + // Identity stored to decide "already up to date": the version ref for the release + // default, or the override string itself so changing the override reinstalls. + const wantedId = sourceOverride || wantedRef; const refKey = "chronos.piPackageRef"; if (!hasPi()) { const choice = await vscode.window.showWarningMessage( - "Chronos requires pi (an AI agent CLI) and the Chronos pi-package. Install them now in a terminal?", + "Chronos requires pi (an AI agent CLI) and the Chronos pi-package. Install them now?", { modal: true }, "Install", ); if (choice !== "Install") return false; - const terminal = vscode.window.createTerminal({ name: "Chronos setup" }); - terminal.sendText( - `npm install -g ${PI_NPM_PACKAGE} && pi install ${wantedSource} && echo "" && echo "Chronos setup complete. Re-run the Chronos command from the Command Palette."`, + const code = await runSetupTask( + "Install pi + Chronos agent", + `npm install -g ${npmPackage} && pi install ${wantedSource}`, ); - terminal.show(true); - vscode.window.showInformationMessage( - "Chronos setup started in the terminal. Re-run the Chronos command once it completes.", - ); - return false; + if (code !== 0 || !hasPi()) { + vscode.window.showErrorMessage( + `Chronos setup failed (exit code ${code}). Check the "Install pi + Chronos agent" task terminal, then run "Chronos: Install Dependencies" to retry.`, + ); + return false; + } + await context.globalState.update(refKey, wantedId); + return true; } // A local dev checkout (path entry) is managed by the developer building from - // source — never reinstall the pinned GitHub package on top of it. - if (hasLocalChronosCheckout()) return true; + // source — never reinstall the pinned release on top of it. An explicit + // chronos.piPackageSource override opts back in (the dev is asking to install + // that source), so it skips this guard and reconciles the registration below. + if (!sourceOverride && hasLocalChronosCheckout()) return true; // Up to date only when the package is actually registered AND at the ref this // extension wants. Gating on actual presence (not just the stored ref) lets a // failed prior install recover on the next launch instead of being skipped. const installed = hasChronosPiPackage(); - if (installed && context.globalState.get(refKey) === wantedRef) return true; + if (installed && context.globalState.get(refKey) === wantedId) return true; // (Re)install at the wanted ref. Covers a fresh install, an extension upgrade // (stored ref differs), and migrating off the legacy history-agent URL (removed // first, since pi keys identity on the URL and would otherwise keep both as // distinct packages). Prompt only when nothing is installed yet; else refresh - // silently. The old URL redirects to the same repo, so the remove+install is a - // safe no-op clone if the migration is all that changed. + // without prompting (the task terminal still shows progress). The old URL + // redirects to the same repo, so the remove+install is a safe no-op clone if the + // migration is all that changed. if (!installed) { const choice = await vscode.window.showInformationMessage( "Install the Chronos pi-package? (one-time registration with pi)", @@ -140,22 +187,52 @@ async function ensureBootstrap(context: vscode.ExtensionContext): Promise isChronosEntry(s) && s !== sourceOverride) + : hasLegacyPiPackage() + ? [LEGACY_PI_PACKAGE] + : []; + const migrate = removals.map((r) => `pi remove ${r} && `).join(""); + const code = await runSetupTask( + `Set up Chronos pi-package (${wantedId})`, + `${migrate}pi install ${wantedSource}`, ); - return false; + if (code !== 0 || !hasChronosPiPackage()) { + vscode.window.showErrorMessage( + `Chronos pi-package setup failed (exit code ${code}). Check the task terminal, then run "Chronos: Install Dependencies" to retry.`, + ); + return false; + } + await context.globalState.update(refKey, wantedId); + return true; +} + +// Walkthrough step checkmarks are driven by context keys: each `completionEvent` +// in package.json watches one of these. Recompute them from real state (pi/pkg +// presence, workspace scaffold, a stored provider key) so steps already satisfied +// show as done. Cheap; called on activation and after the setup/init/login commands. +function refreshWalkthroughContext(workspaceFolder: string | undefined): void { + const depsInstalled = hasPi() && hasChronosPiPackage(); + const workspaceInitialized = !!workspaceFolder && existsSync(join(workspaceFolder, ".chronos")); + const providerConnected = !!workspaceFolder && hasProviderKey(join(workspaceFolder, ".chronos", ".env")); + void vscode.commands.executeCommand("setContext", "chronos.dependenciesInstalled", depsInstalled); + void vscode.commands.executeCommand("setContext", "chronos.workspaceInitialized", workspaceInitialized); + void vscode.commands.executeCommand("setContext", "chronos.providerConnected", providerConnected); +} + +// True when .chronos/.env holds at least one non-empty *_API_KEY / *_API_TOKEN — +// the signal that a provider has been connected (comment lines don't parse as vars). +function hasProviderKey(envPath: string): boolean { + return Object.entries(readEnvFile(envPath)).some(([k, v]) => /_API_(KEY|TOKEN)$/.test(k) && v.length > 0); } function readEnvFile(envPath: string): Record { @@ -630,12 +707,16 @@ export function activate(context: vscode.ExtensionContext): { return; } + // ensureBootstrap runs any install as an awaited task and returns true only + // once deps are present, so a false result means the user declined or the + // install failed (it already surfaced the error) — just stop. if (!(await ensureBootstrap(context))) return; + refreshWalkthroughContext(workspaceFolder); const workspaceEnv = readEnvFile(join(workspaceFolder, ".chronos", ".env")); - // Wait for the HTTP server to be ready so the port is assigned - await httpServer.ready; + // Bind the HTTP server (lazily) so the port is assigned before the agent starts. + await httpServer.start(); const agentEnv = { CHRONOS_HTTP_PORT: String(httpServer.port), @@ -770,6 +851,20 @@ export function activate(context: vscode.ExtensionContext): { } }), + // Machine-level setup: install the pi CLI + register the Chronos pi-package. + // Independent of any workspace; safe to re-run to repair or upgrade. Kept + // separate from `chronos.init` (which only scaffolds a workspace folder) so the + // two scopes don't entangle. Session start still lazily ensures deps too. + vscode.commands.registerCommand("chronos.setup", async () => { + const ready = await ensureBootstrap(context); + refreshWalkthroughContext(workspaceFolder); + // On success, confirm. On failure/decline ensureBootstrap has already surfaced + // the error (or the user opted out) — nothing more to do. + if (ready) { + vscode.window.showInformationMessage("Chronos: dependencies are installed and ready."); + } + }), + vscode.commands.registerCommand("chronos.init", async () => { if (!workspaceFolder) { vscode.window.showWarningMessage( @@ -778,10 +873,9 @@ export function activate(context: vscode.ExtensionContext): { return; } - if (!(await ensureBootstrap(context))) return; - const success = await initWorkspace(workspaceFolder); if (success) { + refreshWalkthroughContext(workspaceFolder); vscode.window.showInformationMessage( "Chronos workspace initialized. Add source directories to sources/ to get started." ); @@ -797,6 +891,7 @@ export function activate(context: vscode.ExtensionContext): { return; } await panel.promptLogin(); + refreshWalkthroughContext(workspaceFolder); }), vscode.commands.registerCommand("chronos.windowSetup", async () => { @@ -816,6 +911,22 @@ export function activate(context: vscode.ExtensionContext): { { dispose: () => httpServer.dispose() } ); + // Reflect current setup state in the Getting Started walkthrough checkmarks. + refreshWalkthroughContext(workspaceFolder); + + // First run on a machine without pi: open the walkthrough once so setup is + // discoverable. Gated on missing pi so we never nag users who are already set up, + // and skipped under the integration test (mock pi, fresh profile) for determinism. + const WALKTHROUGH_SEEN = "chronos.walkthroughSeen"; + if (process.env.CHRONOS_SKIP_BOOTSTRAP !== "1" && !context.globalState.get(WALKTHROUGH_SEEN) && !hasPi()) { + void context.globalState.update(WALKTHROUGH_SEEN, true); + void vscode.commands.executeCommand( + "workbench.action.openWalkthrough", + `${context.extension.id}#chronos.gettingStarted`, + false, + ); + } + // Surface any imports interrupted by a previous crash so they don't fail silently. if (workspaceFolder && existsSync(join(workspaceFolder, "sources"))) { void promptRecoverIncompleteImports(workspaceFolder); diff --git a/chronos-vscode/src/http-server.ts b/chronos-vscode/src/http-server.ts index 2711561..17f6b0e 100644 --- a/chronos-vscode/src/http-server.ts +++ b/chronos-vscode/src/http-server.ts @@ -2,22 +2,27 @@ import { createServer, type Server, type IncomingMessage, type ServerResponse } import type { AgentToExtensionMessage } from "./protocol"; export class HttpServer { - private server: Server; + private server: Server | null = null; private handler: ((msg: AgentToExtensionMessage) => void) | null = null; + private startPromise: Promise | null = null; port = 0; - readonly ready: Promise; - constructor() { + // Bind the loopback server lazily — only when the first agent session starts — + // so merely activating the extension (e.g. on startup, to drive the Getting + // Started walkthrough) opens no port. Idempotent: repeated calls share one server. + start(): Promise { + if (this.startPromise) return this.startPromise; this.server = createServer((req, res) => this.handleRequest(req, res)); - this.ready = new Promise((resolve) => { - this.server.listen(0, "127.0.0.1", () => { - const addr = this.server.address(); + this.startPromise = new Promise((resolve) => { + this.server!.listen(0, "127.0.0.1", () => { + const addr = this.server!.address(); if (addr && typeof addr === "object") { this.port = addr.port; } resolve(); }); }); + return this.startPromise; } private handleRequest(req: IncomingMessage, res: ServerResponse): void { @@ -47,6 +52,6 @@ export class HttpServer { } dispose(): void { - this.server.close(); + this.server?.close(); } } diff --git a/chronos-vscode/src/panel/chronos-panel.ts b/chronos-vscode/src/panel/chronos-panel.ts index 8e79aae..08d6360 100644 --- a/chronos-vscode/src/panel/chronos-panel.ts +++ b/chronos-vscode/src/panel/chronos-panel.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import { execSync } from "node:child_process"; +import { resolvePiBin } from "../pi-env"; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join, dirname, basename, isAbsolute } from "node:path"; @@ -249,33 +249,9 @@ export class ChronosPanel { // ── agent lifecycle ─────────────────────────────────────────────────────── - private resolvePiBin(): string { - const configured = vscode.workspace.getConfiguration("chronos").get("piPath"); - if (configured?.trim()) return configured.trim(); - // GUI-launched VS Code doesn't source shell rc files, so PATH may miss the - // npm global bin dir. Probe PATH first, then common install locations. - try { - execSync(process.platform === "win32" ? "where pi" : "command -v pi", { stdio: "ignore" }); - return "pi"; - } catch { - const home = homedir(); - const candidates = [ - join(home, ".npm-global", "bin", "pi"), - join(home, ".local", "bin", "pi"), - join(home, ".npm", "bin", "pi"), - "/usr/local/bin/pi", - "/opt/homebrew/bin/pi", - ]; - for (const candidate of candidates) { - if (existsSync(candidate)) return candidate; - } - return "pi"; // let spawn fail with a clear ENOENT - } - } - private async startAgent(): Promise { const rpc = new PiRpcSession({ - piBin: this.resolvePiBin(), + piBin: resolvePiBin(), workspaceDir: this.workspaceDir, env: this.agentEnv, }); diff --git a/chronos-vscode/src/pi-env.ts b/chronos-vscode/src/pi-env.ts new file mode 100644 index 0000000..3098987 --- /dev/null +++ b/chronos-vscode/src/pi-env.ts @@ -0,0 +1,44 @@ +import * as vscode from "vscode"; +import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Single source of truth for locating the `pi` binary. Detection (hasPi) and the +// agent launcher (PiRpcSession) MUST resolve it the same way — otherwise a working +// install that isn't on GUI-launched VS Code's minimal PATH reads as "missing", +// triggering redundant install prompts and a perpetually-unchecked setup step. +export function resolvePiBin(): string { + const configured = vscode.workspace.getConfiguration("chronos").get("piPath"); + if (configured?.trim()) return configured.trim(); + // GUI-launched VS Code doesn't source shell rc files, so PATH may miss the + // npm global bin dir. Probe PATH first, then common install locations. + try { + execSync(process.platform === "win32" ? "where pi" : "command -v pi", { stdio: "ignore" }); + return "pi"; + } catch { + const home = homedir(); + const candidates = [ + join(home, ".npm-global", "bin", "pi"), + join(home, ".local", "bin", "pi"), + join(home, ".npm", "bin", "pi"), + "/usr/local/bin/pi", + "/opt/homebrew/bin/pi", + ]; + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate; + } + return "pi"; // let the caller fail with a clear ENOENT + } +} + +// True when pi is actually runnable, resolved exactly the way the agent launches +// it — so "is pi present?" can never disagree with "how do we run pi?". +export function hasPi(): boolean { + try { + execSync(`"${resolvePiBin()}" --version`, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} diff --git a/chronos-vscode/walkthroughs/init.md b/chronos-vscode/walkthroughs/init.md new file mode 100644 index 0000000..ab02432 --- /dev/null +++ b/chronos-vscode/walkthroughs/init.md @@ -0,0 +1,19 @@ +# Initialize a workspace + +A Chronos **workspace** is just a folder. Open the folder you want to work in +(`File ▸ Open Folder…`), then run **Init workspace** to scaffold it: + +``` +sources/ your source directories (each with a png/ subfolder) +data/ per-source extraction results and outputs +memory/ agent memory (MEMORY.MD + per-source notes) +skills/ reusable task instructions (SKILL.md) +sessions/ agent session logs (auto-generated) +.chronos/ provider keys (.env) and settings +``` + +Existing files are never overwritten, so it's safe to re-run on a folder that's +already set up. + +Next, add pages under `sources/` manually, or use **Chronos: Import Sources** to +convert PDFs and images automatically. diff --git a/chronos-vscode/walkthroughs/login.md b/chronos-vscode/walkthroughs/login.md new file mode 100644 index 0000000..d22339c --- /dev/null +++ b/chronos-vscode/walkthroughs/login.md @@ -0,0 +1,16 @@ +# Connect an AI provider + +Chronos works with any provider `pi` supports — **Anthropic, Google, OpenAI**, +and more. No provider is hardcoded. + +The easiest way: click **Log in** in the Chronos panel header and follow the +prompt. Your key is written to `.chronos/.env` in the workspace as a +`_API_KEY` (for example `ANTHROPIC_API_KEY=…`). + +You can also: + +- Run **Chronos: Connect AI Provider (Log In)** from the Command Palette while a + session is open. +- Edit `.chronos/.env` directly. + +The checkmark appears once a provider key is present in the workspace. diff --git a/chronos-vscode/walkthroughs/setup.md b/chronos-vscode/walkthroughs/setup.md new file mode 100644 index 0000000..e0a67dc --- /dev/null +++ b/chronos-vscode/walkthroughs/setup.md @@ -0,0 +1,19 @@ +# Install dependencies + +Chronos drives the [`pi`](https://github.com/badlogic/pi-mono) agent CLI and loads +the **Chronos pi-package** into it. Both install once per machine. + +Running **Install dependencies** opens a terminal that runs: + +``` +npm install -g @earendil-works/pi-coding-agent +pi install https://github.com/ai-historian/chronos@v +``` + +- Already have them? The command detects that and just confirms you're ready. +- The pi-package is pinned to this extension's version, so upgrading the + extension re-installs the matching agent automatically. +- This step is safe to re-run anytime to **repair or upgrade** your install. + +The checkmark appears once `pi` and the package are detected — a few seconds +after the terminal finishes. diff --git a/chronos-vscode/walkthroughs/start.md b/chronos-vscode/walkthroughs/start.md new file mode 100644 index 0000000..8304f86 --- /dev/null +++ b/chronos-vscode/walkthroughs/start.md @@ -0,0 +1,14 @@ +# Start an agent session + +**Start session** opens the Chronos panel: the **page viewer** on one side and +the **chat** with the agent on the other. + +If dependencies aren't installed yet, starting a session will offer to set them +up first — so this is also a fine place to begin. + +Once the panel is open: + +- Pick a source from the header dropdown, or type `/select-source` in the chat. +- The page viewer follows the agent as it reads and cites pages. +- Use **Chronos: Window Setup** to arrange the panel and editor for a clean + two-pane layout. From 2b9d84391c323fa9de52bf4dda58b2f82fb3b722 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Tue, 23 Jun 2026 12:53:13 +0200 Subject: [PATCH 10/17] refactor(chronos): migrate pi peer deps to @earendil-works (0.79) @mariozechner/pi-{coding-agent,ai} are deprecated (frozen at 0.73.x); the maintained packages are @earendil-works/pi-{coding-agent,ai} (same author, a scope rename). Move the pi-package's build off the deprecated scope. - Rename both peer deps and all 17 import specifiers across tools/ and utils/; reinstall (lockfile updated). - Reconcile event handlers with the 0.79 API: session_switch folded into session_start (now carries `reason`); the session_directory hook is removed (pi >=0.7x dropped it -- PI_CODING_AGENT_SESSION_DIR replaces it). Both hooks were already non-firing on the 0.79 runtime. - Update CLAUDE.md and README to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 2 +- chronos/extensions/index.ts | 37 +- chronos/package-lock.json | 6089 +++++++++++++++--------------- chronos/package.json | 4 +- chronos/tools/change-source.ts | 2 +- chronos/tools/expert-registry.ts | 2 +- chronos/tools/expert-tools.ts | 2 +- chronos/tools/expert-turn.ts | 4 +- chronos/tools/list-pages.ts | 2 +- chronos/tools/show-page.ts | 2 +- chronos/tools/show-text.ts | 2 +- chronos/tools/task-batch.ts | 2 +- chronos/tools/view-page.ts | 2 +- chronos/utils/expert-store.ts | 2 +- chronos/utils/resolve-model.ts | 4 +- chronos/utils/session-namer.ts | 4 +- 17 files changed, 3108 insertions(+), 3056 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7f461f5..3a96c87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ Key tools: `task`/`task_batch` (spawn persistent vision-expert subagents per pag - **pi loads the agent from `dist/`, not the TS source.** The `pi` field points extensions at `./dist/extensions`. After editing anything under `chronos/{extensions,tools,utils,http}/*.ts` you MUST `cd chronos && npm run build` or the change won't take effect. Prompts and skills (`chronos/prompts/`, workspace `skills/`) are read live — no rebuild needed. - **Which package pi runs is set in `~/.pi/agent/settings.json` `packages`.** Released use points at the GitHub repo (cloned under `~/.pi/agent/git/`). For local development, point that entry at the absolute path of the `chronos/` dir so sessions run your working copy (after building). Sessions snapshot at startup — restart the session to pick up agent changes. -- **Runtime pi ≠ build-time pi.** The package builds against the `@mariozechner/pi-coding-agent` peer dep, but the installed `pi` binary may be a renamed fork (`@earendil-works/pi-coding-agent`) at a newer version. When debugging pi behavior, read the binary's actual install, not `chronos/node_modules`. +- **pi packages were renamed.** `@mariozechner/pi-{coding-agent,ai}` are deprecated (frozen at 0.73.x); the maintained packages are `@earendil-works/pi-{coding-agent,ai}` (same author, a scope rename). Both the pi-package's peer deps and the installed `pi` binary now use `@earendil-works`. **Runtime pi ≠ build-time pi:** the build-time package in `chronos/node_modules` and the globally-installed `pi` binary can still drift in version — when debugging pi behavior, read the binary's actual install, not `chronos/node_modules`. The 0.79 API differs from 0.73 (e.g. `session_switch`/`session_directory` hooks were dropped — `session_start` now carries a `reason`, and `PI_CODING_AGENT_SESSION_DIR` replaces the session-dir hook). - **Every `.md` in `chronos/prompts/` becomes a slash command** because pi auto-registers prompt templates. Only `select-source` and `yolo` are real user commands; the rest are tool text / the system prompt. The webview slash-command menu therefore filters out `source: "prompt"` entries with `sourceInfo.origin === "package"` (see `chronos-panel.ts`). ### Workspace layout (user-facing, created by `Chronos: Init Workspace`) diff --git a/README.md b/README.md index bc8059b..4456936 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ If you'd rather install everything by hand: ```bash # 1. Install the pi agent globally -npm install -g @mariozechner/pi-coding-agent +npm install -g @earendil-works/pi-coding-agent # 2. Register the Chronos pi-package pi install https://github.com/ai-historian/chronos diff --git a/chronos/extensions/index.ts b/chronos/extensions/index.ts index 04e77fa..1559fc1 100644 --- a/chronos/extensions/index.ts +++ b/chronos/extensions/index.ts @@ -1,5 +1,5 @@ -import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { isToolCallEventType } from "@earendil-works/pi-coding-agent"; import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs"; import { join, basename, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -173,29 +173,30 @@ export default function (pi: ExtensionAPI) { } }; - pi.on("session_start", async (_event, ctx) => { + pi.on("session_start", async (event, ctx) => { ensureWorkspace(ctx.cwd); // .chronos/.env holds workspace API keys (e.g. GEMINI_API_KEY) — the // expert models read them from the environment (and we need them to // re-resolve expert models when restoring sessions). dotenvConfig({ path: join(ctx.cwd, ".chronos", ".env") }); + // session_start fires for the initial startup AND every switch/resume/fork + // (0.79 folded the former session_switch event into this, distinguished by + // event.reason). On a switch the in-memory source + experts belong to the + // previous session — clear them before restoring the target session's state. + if (event.reason !== "startup") { + sourceCtx.sourceDir = null; + sourceCtx.sourceName = null; + sourceCtx.sourceDataDir = null; + expertRegistry.sessions.clear(); + expertRegistry.nextId = 1; + } + // session_shutdown clears the viewer HTTP flag before each switch, so + // (re)connect on every start. connectHttp is idempotent. connectHttp(); restoreSource(ctx); await restoreExperts(ctx); }); - // Switching/resuming a session swaps the active source and experts: clear the - // in-memory state, then restore whatever the target session had (no-op if none). - pi.on("session_switch", async (_event, ctx) => { - sourceCtx.sourceDir = null; - sourceCtx.sourceName = null; - sourceCtx.sourceDataDir = null; - expertRegistry.sessions.clear(); - expertRegistry.nextId = 1; - restoreSource(ctx); - await restoreExperts(ctx); - }); - pi.on("session_shutdown", async () => { disconnectHttp(); }); @@ -208,9 +209,9 @@ export default function (pi: ExtensionAPI) { void maybeNameSession(ctx); }); - pi.on("session_directory", async (event) => { - return { sessionDir: join(event.cwd, "sessions") }; - }); + // (No session_directory hook: pi >= 0.7x dropped it. Session storage is + // redirected via the PI_CODING_AGENT_SESSION_DIR env var, set by the VS Code + // extension when launching pi.) // ── System prompt injection (every turn) ─────────────────────────────── diff --git a/chronos/package-lock.json b/chronos/package-lock.json index 00816b3..b37fb6d 100644 --- a/chronos/package-lock.json +++ b/chronos/package-lock.json @@ -1,12 +1,12 @@ { "name": "chronos", - "version": "0.1.1", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chronos", - "version": "0.1.1", + "version": "0.2.1", "dependencies": { "dotenv": "^16.4.0", "sharp": "^0.34.5" @@ -18,32 +18,11 @@ "typescript": "^5.7.0" }, "peerDependencies": { - "@mariozechner/pi-ai": "*", - "@mariozechner/pi-coding-agent": "*", + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", "@sinclair/typebox": "*" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.73.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", - "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", - "license": "MIT", - "peer": true, - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -75,47 +54,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", @@ -153,272 +91,217 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "node_modules/@aws-sdk/core": { + "version": "3.974.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.23.tgz", + "integrity": "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.31", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1009.0.tgz", - "integrity": "sha512-0k9d0oO6nw3Y6jtgs1cmMPNuwAVPQahIoshKK3NDfhVQR1wNC90/gSpdfa9GKswe8XRq/ZZlq7ny0qM1rd/Hkg==", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.49.tgz", + "integrity": "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/eventstream-handler-node": "^3.972.11", - "@aws-sdk/middleware-eventstream": "^3.972.8", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/middleware-websocket": "^3.972.13", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/token-providers": "3.1009.0", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/eventstream-serde-browser": "^4.2.12", - "@smithy/eventstream-serde-config-resolver": "^4.3.12", - "@smithy/eventstream-serde-node": "^4.2.12", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-stream": "^4.5.19", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.51.tgz", + "integrity": "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", - "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", - "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.56.tgz", + "integrity": "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-login": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", - "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.55.tgz", + "integrity": "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.58", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.58.tgz", + "integrity": "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-ini": "^3.972.56", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", - "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.49.tgz", + "integrity": "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", - "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.55.tgz", + "integrity": "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/token-providers": "3.1074.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", - "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1074.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1074.0.tgz", + "integrity": "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/token-providers": "3.1009.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -426,18 +309,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", - "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.55.tgz", + "integrity": "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -445,15 +327,15 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.11.tgz", - "integrity": "sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.22.tgz", + "integrity": "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -461,2506 +343,3652 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.8.tgz", - "integrity": "sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.18.tgz", + "integrity": "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.31.tgz", + "integrity": "sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">= 14.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.23.tgz", + "integrity": "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", - "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.12", + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.13.tgz", - "integrity": "sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==", + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-format-url": "^3.972.8", - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/eventstream-serde-browser": "^4.2.12", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">= 14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "node_modules/@aws-sdk/types": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", - "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.31.tgz", + "integrity": "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "license": "Apache-2.0", "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", - "license": "Apache-2.0", + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", "peer": true, - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", - "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "integrity": "sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==", + "license": "MIT", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-ai/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", "peer": true, "dependencies": { - "tslib": "^2.6.2" + "json-schema-to-ts": "^3.1.1" }, - "engines": { - "node": ">=20.0.0" + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "node_modules/@earendil-works/pi-ai/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.10.tgz", + "integrity": "sha512-YxaRhmgyDTvLDdGVbe7YzTHV80oL5mX5odg6EhGHz3w5Wu1Ix8DCw7bhtiOBLGQNFRcknia0zPmVWIj30XP1EA==", + "hasShrinkwrap": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.1.38", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", "peer": true, "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "tslib": "^2.6.2" + "json-schema-to-ts": "^3.1.1" }, - "engines": { - "node": ">=20.0.0" + "bin": { + "anthropic-ai-sdk": "bin/cli" }, "peerDependencies": { - "aws-crt": ">=1.0.0" + "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { - "aws-crt": { + "zod": { "optional": true } } }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", - "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.4.1", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", "peer": true, - "engines": { - "node": ">=18.0.0" + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "dependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "tslib": "^2.4.0" + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "peer": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-ai": "^0.79.10", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", "cpu": [ - "ppc64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", "cpu": [ - "s390x" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", "cpu": [ - "x64" + "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" + "win32" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=8.0.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=14" } }, - "node_modules/@google/genai": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.45.0.tgz", - "integrity": "sha512-+sNRWhKiRibVgc4OKi7aBJJ0A7RcoVD8tGG+eFkqxAWRjASDW+ktS9lLwTDnAxZICzCVoeAdu8dYLJVTX60N9w==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "license": "MIT", + "peer": true }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT", + "peer": true }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause", + "peer": true }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "node": ">=0.3.1" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "license": "MIT", + "peer": true, + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "license": "MIT", + "peer": true, + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "engines": { + "node": "^12.20 || >= 14.13" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "engines": { + "node": ">=12.20.0" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "peer": true, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", "peer": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "peer": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=14" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", "peer": true }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "peer": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", "peer": true, "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz", - "integrity": "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", - "optional": true, "peer": true, - "engines": { - "node": ">= 10" + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.2", - "@mariozechner/clipboard-darwin-universal": "0.3.2", - "@mariozechner/clipboard-darwin-x64": "0.3.2", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-musl": "0.3.2", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" + "engines": { + "node": ">= 14" } }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", - "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", - "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "peer": true, "engines": { - "node": ">= 10" + "node": ">= 4" } }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", - "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "peer": true, - "engines": { - "node": ">= 10" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", - "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "peer": true, - "engines": { - "node": ">= 10" + "dependencies": { + "bignumber.js": "^9.0.0" } }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", - "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=16" } }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", - "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", - "cpu": [ - "riscv64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "peer": true, - "engines": { - "node": ">= 10" + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", - "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "peer": true, - "engines": { - "node": ">= 10" + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", - "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", "peer": true, "engines": { - "node": ">= 10" + "node": "20 || >=22" } }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", - "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "peer": true, + "bin": { + "marked": "bin/marked.js" + }, "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", - "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT", - "optional": true, - "os": [ - "win32" + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } ], + "license": "MIT", "peer": true, "engines": { - "node": ">= 10" + "node": ">=10.5.0" } }, - "node_modules/@mariozechner/jiti": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", - "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "peer": true, "dependencies": { - "std-env": "^3.10.0", - "yoctocolors": "^2.1.2" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "peer": true, "bin": { - "jiti": "lib/jiti-cli.mjs" + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, - "node_modules/@mariozechner/pi-agent-core": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.62.0.tgz", - "integrity": "sha512-SBjqgDrgKOaC+IGzFGB3jXQErv9H1QMYnWFvUg6ra6dG0ZgWFBUZb6unidngWLsmaxSDWes6KeKiVFMsr2VSEQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "license": "MIT", "peer": true, "dependencies": { - "@mariozechner/pi-ai": "^0.62.0" + "@types/retry": "0.12.0", + "retry": "^0.13.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=8" } }, - "node_modules/@mariozechner/pi-ai": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.62.0.tgz", - "integrity": "sha512-mJgryZ5RgBQG++tiETMtCQQJoH2MAhKetCfqI98NMvGydu7L9x2qC2JekQlRaAgIlTgv4MRH1UXHMEs4UweE/Q==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", "peer": true, - "dependencies": { - "@anthropic-ai/sdk": "^0.73.0", - "@aws-sdk/client-bedrock-runtime": "^3.983.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "1.14.1", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "openai": "6.26.0", - "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", - "undici": "^7.19.1", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" } }, - "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.62.0.tgz", - "integrity": "sha512-f1NnExqsHuA6w8UVlBtPsvTBhdkMc0h1JD9SzGCdWTLou5GHJr2JIP6DlwV9IKWAnM+sAelaoFez+14wLP2zOQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.62.0", - "@mariozechner/pi-ai": "^0.62.0", - "@mariozechner/pi-tui": "^0.62.0", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", - "diff": "^8.0.2", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", - "glob": "^13.0.1", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "marked": "^15.0.12", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", - "undici": "^7.19.1", - "yaml": "^2.8.2" - }, - "bin": { - "pi": "dist/cli.js" - }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=20.6.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.2" + "node": ">=8" } }, - "node_modules/@mariozechner/pi-tui": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.62.0.tgz", - "integrity": "sha512-/At11PPe8l319MnUoK4wN5L/uVCU6bDdiIUzH8Ez0stOkjSF6isRXScZ+RMM+6iCKsD4muBTX8Cmcif+3/UWHA==", - "license": "MIT", + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", "peer": true, "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=20.0.0" + "node": "18 || 20 || >=22" }, - "optionalDependencies": { - "koffi": "^2.9.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@mistralai/mistralai": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.1.tgz", - "integrity": "sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", "peer": true, "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.24.1" + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", - "optional": true, "peer": true, "engines": { - "node": ">=14" + "node": ">= 4" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, "license": "BSD-3-Clause", "peer": true, "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause", - "peer": true + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "peer": true }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", "peer": true }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause", + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", "peer": true }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", "peer": true }, - "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", "license": "MIT", "peer": true }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", - "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=22.19.0" } }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", "peer": true, - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 8" } }, - "node_modules/@smithy/core": { - "version": "3.23.11", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.11.tgz", - "integrity": "sha512-952rGf7hBRnhUIaeLp6q4MptKW8sPFe5VvkoZ5qIzFAtx6c/QZ/54FS3yootsyUSf9gJX/NBqEBNdNR7jMIlpQ==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" }, "engines": { - "node": ">=18.0.0" + "node": ">= 8" } }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", "peer": true, - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", - "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", "peer": true, - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.1", - "@smithy/util-hex-encoding": "^4.2.2", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", - "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", "peer": true, - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" + "bin": { + "yaml": "bin.mjs" }, "engines": { - "node": ">=18.0.0" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", - "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", - "license": "Apache-2.0", + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" } }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", - "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", - "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, - "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.25", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.25.tgz", - "integrity": "sha512-dqjLwZs2eBxIUG6Qtw8/YZ4DvzHGIf0DA18wrgtfP6a50UIO7e2nY0FPdcbv5tVJKqWCCU5BmGMOUwT7Puan+A==", - "license": "Apache-2.0", - "peer": true, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@smithy/core": "^3.23.11", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.42", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.42.tgz", - "integrity": "sha512-vbwyqHRIpIZutNXZpLAozakzamcINaRCpEy1MYmK6xBeW3xN+TyPRA123GjXnuxZIjc9848MRRCugVMTXxC4Eg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.14.tgz", - "integrity": "sha512-+CcaLoLa5apzSRtloOyG7lQvkUw2ZDml3hRh4QiG9WyEPfW5Ke/3tPOPiPjUneuT59Tpn8+c3RVaUvvkkwqZwg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/core": "^3.23.11", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "license": "Apache-2.0", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", "peer": true, "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" } }, - "node_modules/@smithy/node-http-handler": { - "version": "4.4.16", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.16.tgz", - "integrity": "sha512-ULC8UCS/HivdCB3jhi+kLFYe4B5gxH2gi9vHBfEIiRrT2jfKiZNiETJSlzRtE6B26XbBHjPtc8iZKSNqMol9bw==", - "license": "Apache-2.0", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "peer": true, - "dependencies": { - "@smithy/abort-controller": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "license": "Apache-2.0", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "peer": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "peer": true, "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "license": "Apache-2.0", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "peer": true, "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz", + "integrity": "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==", + "license": "MIT", + "optional": true, "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.2", + "@mariozechner/clipboard-darwin-universal": "0.3.2", + "@mariozechner/clipboard-darwin-x64": "0.3.2", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-x64-musl": "0.3.2", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" } }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", + "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", + "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "peer": true, - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", + "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.5.tgz", - "integrity": "sha512-UqwYawyqSr/aog8mnLnfbPurS0gi4G7IYDcD28cUIBhsvWs1+rQcL2IwkUQ+QZ7dibaoRzhNF99fAQ9AUcO00w==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", + "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@smithy/core": "^3.23.11", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", + "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", + "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", + "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", + "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", - "license": "Apache-2.0", + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", + "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 10" } }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", "peer": true, - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=8.0.0" } }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", "license": "Apache-2.0", "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=14" } }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.41", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.41.tgz", - "integrity": "sha512-M1w1Ux0rSVvBOxIIiqbxvZvhnjQ+VUjJrugtORE90BbadSTH+jsQL279KRL3Hv0w69rE7EuYkV/4Lepz/NBW9g==", - "license": "Apache-2.0", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, "peer": true, - "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=14" } }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.44", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.44.tgz", - "integrity": "sha512-YPze3/lD1KmWuZsl9JlfhcgGLX7AXhSoaCDtiPntUjNW5/YY0lOHjkcgxyE9x/h5vvS1fzDifMGjzqnNlNiqOQ==", - "license": "Apache-2.0", + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "peer": true, "dependencies": { - "@smithy/config-resolver": "^4.4.11", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, - "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "license": "MIT", + "peer": true + }, + "node_modules/@smithy/core": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.26.0.tgz", + "integrity": "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.2.tgz", + "integrity": "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==", "license": "Apache-2.0", "peer": true, "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.2.tgz", + "integrity": "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-retry": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", - "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@smithy/util-stream": { - "version": "4.5.19", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.19.tgz", - "integrity": "sha512-v4sa+3xTweL1CLO2UP0p7tvIMH/Rq1X4KKOxd568mpe6LSLMQCnDHs4uv7m3ukpl3HvcN2JH6jiCS0SNRXKP/w==", + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "node_modules/@smithy/signature-v4": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.2.tgz", + "integrity": "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==", "license": "Apache-2.0", "peer": true, "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "peer": true, "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "peer": true, "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "node": ">=14.0.0" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT", - "peer": true - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "license": "MIT", - "peer": true - }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", @@ -2987,17 +4015,6 @@ "@types/node": "*" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3008,41 +4025,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -3072,26 +4054,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT", - "peer": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3123,16 +4085,6 @@ "license": "MIT", "peer": true }, - "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -3163,16 +4115,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3187,84 +4129,10 @@ "license": "MIT", "peer": true, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "license": "ISC", - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/color-convert": { @@ -3330,21 +4198,6 @@ } } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3400,16 +4253,6 @@ "license": "MIT", "peer": true }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "peer": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", @@ -3452,72 +4295,6 @@ "@esbuild/win32-x64": "0.27.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "peer": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3525,97 +4302,6 @@ "license": "MIT", "peer": true }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT", - "peer": true - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/fast-xml-builder": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz", - "integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "peer": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -3640,25 +4326,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-type": { - "version": "21.3.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", - "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3748,45 +4415,6 @@ "node": ">=18" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "peer": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -3800,31 +4428,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "license": "MIT", - "peer": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3878,16 +4481,6 @@ "license": "ISC", "peer": true }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -3939,27 +4532,6 @@ "node": ">= 14" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3970,16 +4542,6 @@ "node": ">= 4" } }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4037,13 +4599,6 @@ "node": ">=16" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -4067,18 +4622,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/koffi": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz", - "integrity": "sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -4096,46 +4639,6 @@ "node": "20 || >=22" } }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -4169,28 +4672,6 @@ "license": "MIT", "peer": true }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4228,27 +4709,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "peer": true, - "dependencies": { - "wrappy": "1" + "url": "https://opencollective.com/node-fetch" } }, "node_modules/openai": { @@ -4287,40 +4748,6 @@ "node": ">=8" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "peer": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4328,30 +4755,6 @@ "license": "BlueOak-1.0.0", "peer": true }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "license": "MIT", - "peer": true - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "peer": true, - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT", - "peer": true - }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -4359,22 +4762,6 @@ "license": "MIT", "peer": true }, - "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4402,13 +4789,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT", - "peer": true - }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -4456,74 +4836,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "peer": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "peer": true - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "peer": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -4746,65 +5058,6 @@ "license": "ISC", "peer": true }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "peer": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "license": "MIT", - "peer": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4922,91 +5175,6 @@ "node": ">=8" } }, - "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "peer": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "peer": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "license": "MIT", - "peer": true, - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -5040,6 +5208,13 @@ "fsevents": "~2.3.3" } }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT", + "peer": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5054,19 +5229,6 @@ "node": ">=14.17" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici": { "version": "7.24.4", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", @@ -5109,24 +5271,6 @@ "node": ">= 8" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -5169,36 +5313,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC", - "peer": true - }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -5221,16 +5335,6 @@ } } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -5247,59 +5351,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "peer": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/chronos/package.json b/chronos/package.json index 487ff77..3fbfebd 100644 --- a/chronos/package.json +++ b/chronos/package.json @@ -13,8 +13,8 @@ "build": "tsc" }, "peerDependencies": { - "@mariozechner/pi-ai": "*", - "@mariozechner/pi-coding-agent": "*", + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", "@sinclair/typebox": "*" }, "dependencies": { diff --git a/chronos/tools/change-source.ts b/chronos/tools/change-source.ts index bc0e04d..94a82ea 100644 --- a/chronos/tools/change-source.ts +++ b/chronos/tools/change-source.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, mkdirSync } from "node:fs"; import { basename, join } from "node:path"; import { Type } from "@sinclair/typebox"; -import type { ToolDefinition, ExtensionContext } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { listPageIds } from "../utils/page-files.js"; import { saveSessionSource } from "../utils/session-source-store.js"; import { sendToExtension } from "../http/http-client.js"; diff --git a/chronos/tools/expert-registry.ts b/chronos/tools/expert-registry.ts index 91e8374..03a2663 100644 --- a/chronos/tools/expert-registry.ts +++ b/chronos/tools/expert-registry.ts @@ -1,4 +1,4 @@ -import type { Api, Message, Model } from "@mariozechner/pi-ai"; +import type { Api, Message, Model } from "@earendil-works/pi-ai"; /** * Registry of expert sessions keyed by task ID. diff --git a/chronos/tools/expert-tools.ts b/chronos/tools/expert-tools.ts index 9a7fe3e..e9da184 100644 --- a/chronos/tools/expert-tools.ts +++ b/chronos/tools/expert-tools.ts @@ -16,7 +16,7 @@ import { exec } from "node:child_process"; import { promisify } from "node:util"; import { resolve, relative, isAbsolute, dirname, join } from "node:path"; import { Type } from "@sinclair/typebox"; -import type { ImageContent, TextContent, Tool, ToolCall, ToolResultMessage } from "@mariozechner/pi-ai"; +import type { ImageContent, TextContent, Tool, ToolCall, ToolResultMessage } from "@earendil-works/pi-ai"; import type { Bbox } from "../utils/crop-image.js"; import { pageImageContent } from "./expert-turn.js"; diff --git a/chronos/tools/expert-turn.ts b/chronos/tools/expert-turn.ts index 42476cd..c7ea708 100644 --- a/chronos/tools/expert-turn.ts +++ b/chronos/tools/expert-turn.ts @@ -6,8 +6,8 @@ import { type TextContent, type ToolCall, type UserMessage, -} from "@mariozechner/pi-ai"; -import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; +} from "@earendil-works/pi-ai"; +import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { pageIdToPath } from "../utils/page-files.js"; import type { ExpertRegistry, ExpertSession } from "./expert-registry.js"; import { newTaskId } from "./expert-registry.js"; diff --git a/chronos/tools/list-pages.ts b/chronos/tools/list-pages.ts index 2e51a5f..1451ed4 100644 --- a/chronos/tools/list-pages.ts +++ b/chronos/tools/list-pages.ts @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { listPageIds } from "../utils/page-files.js"; import { sendToExtension } from "../http/http-client.js"; import type { SourceContext } from "./source-context.js"; diff --git a/chronos/tools/show-page.ts b/chronos/tools/show-page.ts index 27e66d1..227d3df 100644 --- a/chronos/tools/show-page.ts +++ b/chronos/tools/show-page.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { Type } from "@sinclair/typebox"; -import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { pageIdToPath, listPageIds } from "../utils/page-files.js"; import { sendToExtension } from "../http/http-client.js"; import type { SourceContext } from "./source-context.js"; diff --git a/chronos/tools/show-text.ts b/chronos/tools/show-text.ts index afd776b..2389193 100644 --- a/chronos/tools/show-text.ts +++ b/chronos/tools/show-text.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve, isAbsolute, join, basename } from "node:path"; import { Type } from "@sinclair/typebox"; -import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { sendToExtension } from "../http/http-client.js"; import type { SourceContext } from "./source-context.js"; import { requireSource } from "./source-context.js"; diff --git a/chronos/tools/task-batch.ts b/chronos/tools/task-batch.ts index 523d1ef..309b64a 100644 --- a/chronos/tools/task-batch.ts +++ b/chronos/tools/task-batch.ts @@ -1,7 +1,7 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { Type } from "@sinclair/typebox"; -import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; diff --git a/chronos/tools/view-page.ts b/chronos/tools/view-page.ts index cf7d69a..4d2fa34 100644 --- a/chronos/tools/view-page.ts +++ b/chronos/tools/view-page.ts @@ -1,7 +1,7 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { Type } from "@sinclair/typebox"; -import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import type { ExpertRegistry } from "./expert-registry.js"; import type { SourceContext } from "./source-context.js"; import { requireSourceDataDir } from "./source-context.js"; diff --git a/chronos/utils/expert-store.ts b/chronos/utils/expert-store.ts index 3008109..4f76f21 100644 --- a/chronos/utils/expert-store.ts +++ b/chronos/utils/expert-store.ts @@ -14,7 +14,7 @@ */ import { readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync } from "node:fs"; import { join } from "node:path"; -import type { AssistantMessage } from "@mariozechner/pi-ai"; +import type { AssistantMessage } from "@earendil-works/pi-ai"; import type { Bbox } from "./crop-image.js"; import type { PersistedToolResult } from "../tools/expert-tools.js"; diff --git a/chronos/utils/resolve-model.ts b/chronos/utils/resolve-model.ts index 9853779..f4a7109 100644 --- a/chronos/utils/resolve-model.ts +++ b/chronos/utils/resolve-model.ts @@ -1,5 +1,5 @@ -import type { Api, Model } from "@mariozechner/pi-ai"; -import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; +import type { Api, Model } from "@earendil-works/pi-ai"; +import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; export type ResolvedModel = | { ok: true; model: Model; apiKey: string | undefined; headers?: Record } diff --git a/chronos/utils/session-namer.ts b/chronos/utils/session-namer.ts index fc2546e..c2402a1 100644 --- a/chronos/utils/session-namer.ts +++ b/chronos/utils/session-namer.ts @@ -7,8 +7,8 @@ * Only USER prompts are sent — never assistant replies, tool results, or page * images — so the input stays tiny, cheap, and free of extracted content. */ -import { complete, type UserMessage } from "@mariozechner/pi-ai"; -import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { complete, type UserMessage } from "@earendil-works/pi-ai"; +import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; import { pickCheapestModelSpec, resolveExpertModel } from "./resolve-model.js"; const MAX_PROMPTS = 6; From 4e79b7aada832c93e2348d5000cb445f453b9a06 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Thu, 25 Jun 2026 12:54:14 +0200 Subject: [PATCH 11/17] feat(agent): provider-aware cheap-model ladder for session naming (#8) Generate session titles with a small, current model chosen for the orchestrator's provider instead of the registry's blind "cheapest", which ranks decommissioned models first and 404s at call time. helperModelCandidates() returns ordered specs best-first: the orchestrator provider's small tier (haiku/flash/nano), then any auth'd small-tier model from any provider, then the cheapest, then the orchestrator's own model; generateSessionTitle tries each until one succeeds. Hardening from the pre-release review: - rank rolling "*-latest" aliases first (they have no version digit but are always the currently-served model); - when no orchestrator is known, still prefer an auth'd small-tier model over the possibly-retired global cheapest; - defensive cost accessor so a cost-less custom-provider model can't throw the comparator out of the (try/catch-free) candidate builder; - correct the saveSessionName JSDoc, which overstated cross-process write safety (atomic publish only; same-workspace races self-heal on next agent_end). Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos/extensions/index.ts | 2 +- chronos/utils/resolve-model.ts | 91 ++++++++++++++++++++++++++++- chronos/utils/session-name-store.ts | 7 ++- chronos/utils/session-namer.ts | 45 ++++++++------ 4 files changed, 120 insertions(+), 25 deletions(-) diff --git a/chronos/extensions/index.ts b/chronos/extensions/index.ts index 1559fc1..4f89dd1 100644 --- a/chronos/extensions/index.ts +++ b/chronos/extensions/index.ts @@ -305,7 +305,7 @@ async function maybeNameSession(ctx: ExtensionContext): Promise { namingInFlight.add(sessionId); try { - const title = await generateSessionTitle(ctx.modelRegistry, prompts); + const title = await generateSessionTitle(ctx.modelRegistry, prompts, ctx.model); if (title) saveSessionName(ctx.cwd, sessionId, title, target); } finally { namingInFlight.delete(sessionId); diff --git a/chronos/utils/resolve-model.ts b/chronos/utils/resolve-model.ts index f4a7109..c570d66 100644 --- a/chronos/utils/resolve-model.ts +++ b/chronos/utils/resolve-model.ts @@ -30,15 +30,100 @@ async function resolveAuth( * as "provider/model-id", or undefined if none. Used for throwaway helper calls * like session-name summarisation — keep the cost negligible and provider-neutral. */ +function modelCost(m: Model): number { + // Custom/backend providers may register models without a cost; treat as free + // so the comparator never throws on `.input`/`.output` of an absent cost. + return (m.cost?.input ?? 0) + (m.cost?.output ?? 0); +} + export function pickCheapestModelSpec(registry: ModelRegistry): string | undefined { const models = registry.getAvailable(); if (models.length === 0) return undefined; - const cheapest = [...models].sort( - (a, b) => (a.cost.input + a.cost.output) - (b.cost.input + b.cost.output), - )[0]; + const cheapest = [...models].sort((a, b) => modelCost(a) - modelCost(b))[0]; return `${cheapest.provider}/${cheapest.id}`; } +/** + * Small, current model tier to prefer for cheap throwaway helper calls (e.g. + * session-name summarisation), keyed by the orchestrator's provider. The + * registry also lists decommissioned models (e.g. retired claude-3-* ids) and + * ranks them cheapest, so "cheapest overall" alone picks a model that 404s at + * call time — prefer the provider's current small tier instead. + */ +const HELPER_FAMILIES: { match: (provider: string) => boolean; family: RegExp }[] = [ + { match: (p) => p.includes("anthropic"), family: /haiku/i }, + { match: (p) => p.includes("google") || p.includes("gemini") || p.includes("vertex"), family: /flash/i }, + { match: (p) => p.includes("openai") || p.includes("azure"), family: /nano/i }, +]; + +// A model's headline version, parsed from its display name (e.g. "Claude Haiku +// 4.5" -> 4.05, "GPT-5.4 nano" -> 5.04), used to pick the newest — and thus +// still-served — model in a family. Returns -1 when no version is present. +function modelVersion(model: Model): number { + const m = (model.name ?? model.id).match(/(\d+)(?:\.(\d+))?/); + if (!m) return -1; + return Number(m[1]) + (m[2] ? Number(m[2]) / 100 : 0); +} + +// The newest available model in `family` for `provider`, or undefined if none. +function latestFamilyModel(models: Model[], provider: string, family: RegExp): Model | undefined { + const candidates = models.filter((m) => m.provider === provider && family.test(m.id)); + if (candidates.length === 0) return undefined; + // A rolling "*-latest" alias has no version digit (so it would sort last by + // version) but is always the currently-served model — prefer it outright, + // then the highest explicit version, then the cheaper id. + const isLatestAlias = (m: Model) => (/latest/i.test(`${m.name ?? ""} ${m.id}`) ? 0 : 1); + return candidates.sort((a, b) => { + if (isLatestAlias(a) !== isLatestAlias(b)) return isLatestAlias(a) - isLatestAlias(b); + const dv = modelVersion(b) - modelVersion(a); + if (dv !== 0) return dv; + return modelCost(a) - modelCost(b); + })[0]; +} + +/** + * Ordered, deduped candidate specs for a cheap throwaway helper call, best-first: + * 1. the orchestrator provider's current small tier (anthropic → latest Haiku, + * google → latest Flash, openai → latest Nano); + * 2. any auth'd small-tier model (haiku/flash/nano) from any provider — these + * are current/served, so they cover the no-orchestrator path and providers + * with no family match without falling back to a possibly-retired model; + * 3. the cheapest model pi has auth for (the registry lists retired models and + * ranks them cheapest, so this can 404 — last resort); + * 4. the orchestrator's own current model (guaranteed callable). + * The caller tries each in order until one succeeds, so a retired/blocked model + * is skipped rather than failing the call outright. + */ +export function helperModelCandidates( + registry: ModelRegistry, + orchestrator?: { provider: string; id: string }, +): string[] { + const available = registry.getAvailable(); + const specs: string[] = []; + const add = (spec: string | undefined) => { + if (spec && !specs.includes(spec)) specs.push(spec); + }; + const familyBest = (provider: string, family: RegExp) => { + const m = latestFamilyModel(available, provider, family); + if (m) add(`${m.provider}/${m.id}`); + }; + + // 1. The orchestrator provider's current small tier first. + if (orchestrator) { + const fam = HELPER_FAMILIES.find((f) => f.match(orchestrator.provider)); + if (fam) familyBest(orchestrator.provider, fam.family); + } + // 2. Any auth'd small-tier model from any provider (current/served). + for (const fam of HELPER_FAMILIES) { + const providers = new Set(available.filter((m) => fam.match(m.provider)).map((m) => m.provider)); + for (const p of providers) familyBest(p, fam.family); + } + // 3. Cheapest with auth (may be retired) and 4. the orchestrator's own model. + add(pickCheapestModelSpec(registry)); + add(orchestrator ? `${orchestrator.provider}/${orchestrator.id}` : undefined); + return specs; +} + const MAX_LISTED_MODELS = 40; function formatAvailable(registry: ModelRegistry, needsVision: boolean): string { diff --git a/chronos/utils/session-name-store.ts b/chronos/utils/session-name-store.ts index 3f77823..3c8769e 100644 --- a/chronos/utils/session-name-store.ts +++ b/chronos/utils/session-name-store.ts @@ -47,8 +47,11 @@ export function getNamedPromptCount(workspaceDir: string, sessionId: string): nu /** * Cache (or refine) a generated display name. `fromPrompts` records how many * prompts it summarises; a later call with more prompts overwrites it. Writes - * atomically (temp + rename) and re-reads immediately before writing so a - * concurrent writer's entry for a different session isn't clobbered. + * atomically (temp + rename) so a reader never sees a torn file. This does NOT + * serialise concurrent writers in separate processes (e.g. the same workspace + * open in two windows): a same-instant race can drop one session's entry, which + * is regenerated on the next agent_end. Within a single process, naming is + * already serialised by the in-flight guard in maybeNameSession. */ export function saveSessionName(workspaceDir: string, sessionId: string, name: string, fromPrompts: number): void { if (!sessionId || !name) return; diff --git a/chronos/utils/session-namer.ts b/chronos/utils/session-namer.ts index c2402a1..6119a58 100644 --- a/chronos/utils/session-namer.ts +++ b/chronos/utils/session-namer.ts @@ -9,7 +9,7 @@ */ import { complete, type UserMessage } from "@earendil-works/pi-ai"; import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; -import { pickCheapestModelSpec, resolveExpertModel } from "./resolve-model.js"; +import { helperModelCandidates, resolveExpertModel } from "./resolve-model.js"; const MAX_PROMPTS = 6; const MAX_INPUT_CHARS = 4000; @@ -31,14 +31,13 @@ function cleanTitle(raw: string): string | undefined { export async function generateSessionTitle( registry: ModelRegistry, userPrompts: string[], + orchestrator?: { provider: string; id: string }, ): Promise { const prompts = userPrompts.map((p) => p.trim()).filter(Boolean).slice(0, MAX_PROMPTS); if (prompts.length === 0) return undefined; - const spec = pickCheapestModelSpec(registry); - if (!spec) return undefined; - const resolved = await resolveExpertModel(spec, registry, undefined, false); - if (!resolved.ok) return undefined; + const candidates = helperModelCandidates(registry, orchestrator); + if (candidates.length === 0) return undefined; const joined = prompts.map((p, i) => `${i + 1}. ${p}`).join("\n").slice(0, MAX_INPUT_CHARS); const userMessage: UserMessage = { @@ -47,19 +46,27 @@ export async function generateSessionTitle( timestamp: Date.now(), }; - try { - const response = await complete( - resolved.model, - { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, - { apiKey: resolved.apiKey, headers: resolved.headers }, - ); - if (response.stopReason === "error") return undefined; - const text = response.content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - return cleanTitle(text); - } catch { - return undefined; + // Try candidates best-first; skip any that won't resolve or whose call errors + // (e.g. the registry ranks a decommissioned model cheapest — it 404s here). + for (const spec of candidates) { + const resolved = await resolveExpertModel(spec, registry, undefined, false); + if (!resolved.ok) continue; + try { + const response = await complete( + resolved.model, + { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, + { apiKey: resolved.apiKey, headers: resolved.headers }, + ); + if (response.stopReason === "error") continue; + const text = response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + const title = cleanTitle(text); + if (title) return title; + } catch { + continue; + } } + return undefined; } From be93478ad49442368edb85c710c96bc89af4b3e4 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Thu, 25 Jun 2026 12:54:24 +0200 Subject: [PATCH 12/17] =?UTF-8?q?fix(agent):=20harden=20expert=20tooling?= =?UTF-8?q?=20=E2=80=94=20secrets,=20abort,=20bbox=20bounds=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-release review fixes to the vision-expert tool loop: - read_file/list_dir/grep are always-on (no human gate) but resolveInWorkspace only blocked path escapes, so a default read-only expert could read .chronos/.env (provider API keys) and echo them into the transcript. Apply walkFiles' policy (dot-dirs + SKIP_DIRS off-limits) to every path-based tool, and filter those entries out of list_dir. - A cancel that lands while complete() is in flight resolves with an "aborted" response rather than throwing; it was committed to the session, persisted, and returned ok:true — so callers wrote "(empty response)" to output files and the turn was dropped on restore. Treat aborted like a failed turn before pushing/persisting. - Clamp the expert's view_region bbox into bounds in bboxToPixels: the self-zoom bbox isn't schema-validated at call time, so an out-of-range value produced a negative/out-of-bounds extract that made sharp throw. Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos/tools/expert-tools.ts | 11 +++++++++-- chronos/tools/expert-turn.ts | 7 +++++++ chronos/utils/crop-image.ts | 14 ++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/chronos/tools/expert-tools.ts b/chronos/tools/expert-tools.ts index e9da184..25aac7a 100644 --- a/chronos/tools/expert-tools.ts +++ b/chronos/tools/expert-tools.ts @@ -197,14 +197,20 @@ function coerceBbox(value: unknown): Bbox | null { return null; } -// Resolve a workspace-relative path and refuse anything that escapes the root. +// Resolve a workspace-relative path and refuse anything that escapes the root or +// reaches into a restricted directory. Mirrors walkFiles' policy (dot-dirs and +// SKIP_DIRS are off-limits) so the always-on read_file/list_dir/grep tools can't +// read workspace secrets such as .chronos/.env, which the walk path already skips. function resolveInWorkspace(cwd: string, p: string): string { const abs = resolve(cwd, p); const rel = relative(cwd, abs); - if (rel === "" ) return abs; + if (rel === "") return abs; if (rel.startsWith("..") || isAbsolute(rel)) { throw new Error("Path is outside the workspace."); } + if (rel.split(/[\\/]/).some((seg) => seg.startsWith(".") || SKIP_DIRS.has(seg))) { + throw new Error("Path is in a restricted directory."); + } return abs; } @@ -304,6 +310,7 @@ export async function executeExpertTool(call: ToolCall, ctx: ExpertToolContext): try { const abs = p ? resolveInWorkspace(ctx.cwd, p) : ctx.cwd; const entries = readdirSync(abs, { withFileTypes: true }) + .filter((e) => !(e.name.startsWith(".") || SKIP_DIRS.has(e.name))) .map((e) => (e.isDirectory() ? `${e.name}/` : e.name)) .sort(); return text(entries.join("\n") || "(empty directory)", p || "."); diff --git a/chronos/tools/expert-turn.ts b/chronos/tools/expert-turn.ts index c7ea708..f7228f2 100644 --- a/chronos/tools/expert-turn.ts +++ b/chronos/tools/expert-turn.ts @@ -231,6 +231,13 @@ export async function runExpertTurn( error: `Expert model error (${modelSpec(resolved.model)}): ${response.errorMessage ?? "unknown error"}`, }; } + // A cancel that lands while complete() is in flight resolves with an + // "aborted" response rather than throwing. Treat it as a failed turn so it is + // neither committed to session.messages nor persisted, and the callers don't + // write its empty/partial text to an output_file as if it were a real answer. + if (input.signal?.aborted || response.stopReason === "aborted") { + return { ok: false, taskId, error: "Expert turn aborted." }; + } turnMessages.push(response); finalResponse = response; totalCost += response.usage?.cost?.total ?? 0; diff --git a/chronos/utils/crop-image.ts b/chronos/utils/crop-image.ts index d8a854d..6b84017 100644 --- a/chronos/utils/crop-image.ts +++ b/chronos/utils/crop-image.ts @@ -8,10 +8,16 @@ export interface Bbox { } function bboxToPixels(bbox: Bbox, width: number, height: number) { - const left = Math.round(bbox.x * width); - const top = Math.round(bbox.y * height); - const cropW = Math.min(Math.round(bbox.w * width), width - left); - const cropH = Math.min(Math.round(bbox.h * height), height - top); + // Clamp the normalized box into bounds: an expert's view_region bbox isn't + // schema-validated at call time, so an out-of-range value would otherwise + // produce a negative/out-of-bounds extract that makes sharp throw. Keep the + // origin at most one pixel inside each edge so the crop is always ≥ 1px. + const x = Math.min(Math.max(bbox.x, 0), 1); + const y = Math.min(Math.max(bbox.y, 0), 1); + const left = Math.min(Math.round(x * width), width - 1); + const top = Math.min(Math.round(y * height), height - 1); + const cropW = Math.max(1, Math.min(Math.round(bbox.w * width), width - left)); + const cropH = Math.max(1, Math.min(Math.round(bbox.h * height), height - top)); return { left, top, cropW, cropH }; } From 04514aa2288f4ec2e1b9fef95de2288f64ca7fd7 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Thu, 25 Jun 2026 12:54:32 +0200 Subject: [PATCH 13/17] feat(extension): resizable data-viewer columns + provenance hardening (#5) Drag a column header's right-edge handle to resize; double-click to auto-fit (canvas text measure). The first interaction seeds every column's rendered width and flips the table to fixed layout, so untouched columns hold and the table can grow past the viewport with horizontal scroll. Includes pre-release review fixes: - Use pointer capture on drag handles (column resize + preview splitter) so a pointerup released outside the webview still tears down the move listener; without it the listener leaked and the column stuck to the cursor with no recovery short of reload. - Prune colWidths to the live column set on in-place reload (Refresh), so a schema change doesn't leave orphan width keys or silently revert to auto layout on a renamed column. - Multi-reference provenance (#5): a malformed flat numeric bbox (length != 4) now collapses to a single null instead of inflating the chip count, and a lone bbox is no longer broadcast across distinct pages (only across regions of one page), which had mis-cropped the other pages. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../webview/components/data-viewer.ts | 180 +++++++++++++++++- chronos-vscode/webview/styles.css | 50 +++++ 2 files changed, 220 insertions(+), 10 deletions(-) diff --git a/chronos-vscode/webview/components/data-viewer.ts b/chronos-vscode/webview/components/data-viewer.ts index 98d2412..6926a72 100644 --- a/chronos-vscode/webview/components/data-viewer.ts +++ b/chronos-vscode/webview/components/data-viewer.ts @@ -10,6 +10,14 @@ const RESERVED = { source: "chronos_source", } as const; +// Width-map key for the provenance action column (it has no column name of its +// own). Namespaced so it can't collide with a real data column. +const PROV_COL = "__chronos_prov__"; +const MIN_COL_WIDTH = 48; +// Upper bound for double-click auto-fit; matches the cell max-width cap so a +// column with one very long entry doesn't blow out the whole table. +const MAX_COL_WIDTH = 360; + interface Row { [key: string]: unknown; } @@ -73,7 +81,13 @@ function bboxList(value: unknown): (Bbox | null)[] { if (value === undefined || value === null) return []; const single = toBbox(value); if (single) return [single]; - if (Array.isArray(value)) return value.map(toBbox); + if (Array.isArray(value)) { + // A flat array of bare numbers is one (malformed, non-4-tuple) bbox, not a + // parallel list — collapse it to a single null so it doesn't inflate the + // reference count. Only map element-wise for a real list (nested arrays/objects). + if (value.every((el) => typeof el === "number")) return [null]; + return value.map(toBbox); + } return []; } @@ -87,6 +101,10 @@ function rowProvenance(row: Row): Provenance[] { const sources = sourceList(row[RESERVED.source]); const count = Math.max(pages.length, bboxes.length, sources.length); const at = (list: T[], i: number): T | undefined => (list.length === 1 ? list[0] : list[i]); + // A single bbox broadcasts across several regions of ONE page (pages.length===1), + // but not across distinct pages — that region was measured on one page only, so + // applying it to the others would mis-crop them. Drop the bbox in that case. + const multiPage = pages.length > 1; const refs: Provenance[] = []; for (let i = 0; i < count; i++) { @@ -94,7 +112,8 @@ function rowProvenance(row: Row): Provenance[] { // Skip just this slot when the page is missing/invalid — keep every other // reference aligned with its own bbox/source (don't shift the rest). if (pageId === undefined || Number.isNaN(pageId)) continue; - refs.push({ pageId, bbox: at(bboxes, i) ?? null, sourcePath: at(sources, i) ?? undefined }); + const bbox = bboxes.length === 1 && multiPage ? null : at(bboxes, i) ?? null; + refs.push({ pageId, bbox, sourcePath: at(sources, i) ?? undefined }); } return refs; } @@ -116,6 +135,7 @@ export class ChronosDataViewer extends LitElement { sortDir: { state: true }, preview: { state: true }, previewPct: { state: true }, + colWidths: { state: true }, }; declare sourceName: string; @@ -127,6 +147,9 @@ export class ChronosDataViewer extends LitElement { declare sortDir: 1 | -1; declare preview: Preview | null; declare previewPct: number; + // Per-column pixel widths once the user has dragged a resize handle. Empty + // until first resize, when every column is seeded from its rendered width. + declare colWidths: Record; private postMessage: (msg: WebviewToExt) => void = () => {}; @@ -141,6 +164,7 @@ export class ChronosDataViewer extends LitElement { this.sortDir = 1; this.preview = null; this.previewPct = 30; + this.colWidths = {}; } protected createRenderRoot(): HTMLElement { @@ -172,6 +196,27 @@ export class ChronosDataViewer extends LitElement { if (filename !== this.selected) return; this.content = content; this.loading = false; + this.pruneColWidths(content); + } + + // A Refresh reloads the same file in place, and the agent may rewrite an + // extraction output with added/removed/renamed columns. Drop width keys that + // no longer match a live column so a schema change doesn't leave orphan keys or + // (via the `resized` every-column guard) silently revert the table to auto + // layout on a column that merely changed name. + private pruneColWidths(content: string): void { + if (Object.keys(this.colWidths).length === 0) return; + const parsed = this.parse(content); + if (parsed.kind !== "table") { + this.colWidths = {}; + return; + } + const allowed = new Set([...parsed.columns, PROV_COL]); + const next: Record = {}; + for (const [k, v] of Object.entries(this.colWidths)) { + if (allowed.has(k)) next[k] = v; + } + this.colWidths = next; } /** Reset to the empty state (no source bound) — e.g. when a new session starts. */ @@ -182,6 +227,7 @@ export class ChronosDataViewer extends LitElement { this.content = null; this.loading = false; this.preview = null; + this.colWidths = {}; } private selectFile(filename: string | null): void { @@ -189,6 +235,7 @@ export class ChronosDataViewer extends LitElement { this.content = null; this.sortColumn = null; this.preview = null; + this.colWidths = {}; if (filename) { this.loading = true; this.postMessage({ type: "data/load", filename }); @@ -258,6 +305,89 @@ export class ChronosDataViewer extends LitElement { } } + // Drag a header's right-edge handle to resize that column. The first drag + // seeds every column's current rendered width and flips the table to fixed + // layout, so untouched columns stay put and the table can grow past the + // viewport (the scroll container then scrolls horizontally). + private onColResizeDown(e: PointerEvent, column: string): void { + e.preventDefault(); + e.stopPropagation(); // don't trigger the header's sort click + const table = this.querySelector(".dv-table"); + if (!table) return; + const widths = this.seedWidths(table); + const startX = e.clientX; + const startW = widths[column] ?? MIN_COL_WIDTH; + // Capture the pointer on the handle so the drag keeps tracking — and its + // teardown always fires — even when the pointer leaves the webview and the + // button is released outside it. A window-level pointerup is missed in that + // case, which would leak the move listener and glue the column to the cursor. + const handle = e.currentTarget as HTMLElement; + handle.setPointerCapture(e.pointerId); + const onMove = (ev: PointerEvent) => { + const w = Math.max(MIN_COL_WIDTH, Math.round(startW + (ev.clientX - startX))); + this.colWidths = { ...this.colWidths, [column]: w }; + }; + const onUp = () => { + handle.removeEventListener("pointermove", onMove); + handle.removeEventListener("pointerup", onUp); + handle.removeEventListener("lostpointercapture", onUp); + }; + handle.addEventListener("pointermove", onMove); + handle.addEventListener("pointerup", onUp); + handle.addEventListener("lostpointercapture", onUp); // also covers pointercancel + } + + // Double-click the handle to auto-fit the column to its widest cell, capped at + // MAX_COL_WIDTH (a longer single entry then wraps within that width). + private onColResizeDblClick(e: MouseEvent, column: string): void { + e.preventDefault(); + e.stopPropagation(); + const table = this.querySelector(".dv-table"); + const th = (e.currentTarget as HTMLElement).closest("th") as HTMLTableCellElement | null; + if (!table || !th) return; + this.seedWidths(table); // lock the other columns before we change this one + const fit = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, this.measureColumnFit(table, th))); + this.colWidths = { ...this.colWidths, [column]: fit }; + } + + // On first interaction, capture every column's current rendered width so the + // table can flip to fixed layout without the untouched columns shifting. + private seedWidths(table: HTMLTableElement): Record { + if (Object.keys(this.colWidths).length > 0) return this.colWidths; + const seeded: Record = {}; + table.querySelectorAll("thead th[data-col]").forEach((th) => { + const c = th.dataset.col; + if (c) seeded[c] = th.getBoundingClientRect().width; + }); + this.colWidths = seeded; + return seeded; + } + + // Widest of the header label and every body cell in this column, measured + // single-line via canvas (layout-independent), plus cell padding. + private measureColumnFit(table: HTMLTableElement, th: HTMLTableCellElement): number { + const ctx = document.createElement("canvas").getContext("2d"); + if (!ctx) return MIN_COL_WIDTH; + const fontOf = (el: Element | null): string => { + const cs = getComputedStyle(el ?? th); + return `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; + }; + const idx = th.cellIndex; + const label = th.querySelector(".dv-col-label"); + ctx.font = fontOf(label); + let max = ctx.measureText(label?.textContent ?? "").width; + const body = table.tBodies[0]; + if (body) { + ctx.font = fontOf(body.rows[0]?.cells[idx] ?? null); + for (const row of Array.from(body.rows)) { + const text = row.cells[idx]?.textContent ?? ""; + const w = ctx.measureText(text).width; + if (w > max) max = w; + } + } + return Math.round(max + 20 /* 10px padding each side */ + 2 /* anti-ellipsis slack */); + } + // Row "view source": show the cited region inline at the bottom of the data // viewer. Stays here — the source viewer is independent. private viewSource(prov: Provenance): void { @@ -399,17 +529,23 @@ export class ChronosDataViewer extends LitElement { e.preventDefault(); const root = this.querySelector(".dv-root"); if (!root) return; + // Capture on the splitter so a pointerup outside the webview still tears the + // listeners down (a missed window pointerup would leak the move handler). + const handle = e.currentTarget as HTMLElement; + handle.setPointerCapture(e.pointerId); const onMove = (ev: PointerEvent) => { const rect = root.getBoundingClientRect(); // Preview is docked at the bottom: its height is from the pointer to the foot. this.previewPct = Math.max(15, Math.min(70, ((rect.bottom - ev.clientY) / rect.height) * 100)); }; const onUp = () => { - window.removeEventListener("pointermove", onMove); - window.removeEventListener("pointerup", onUp); + handle.removeEventListener("pointermove", onMove); + handle.removeEventListener("pointerup", onUp); + handle.removeEventListener("lostpointercapture", onUp); }; - window.addEventListener("pointermove", onMove); - window.addEventListener("pointerup", onUp); + handle.addEventListener("pointermove", onMove); + handle.addEventListener("pointerup", onUp); + handle.addEventListener("lostpointercapture", onUp); // also covers pointercancel } private renderPreview(): TemplateResult { @@ -480,14 +616,38 @@ export class ChronosDataViewer extends LitElement { private renderTable(parsed: Extract): TemplateResult { const { rows, provenance } = this.sortRows(parsed); const hasProvenance = provenance.some((refs) => refs.length > 0); + // Only switch to fixed layout once every visible column has a recorded width + // — otherwise a column without a width would collapse under fixed layout. + const resized = + Object.keys(this.colWidths).length > 0 && + parsed.columns.every((c) => this.colWidths[c] != null) && + (!hasProvenance || this.colWidths[PROV_COL] != null); + const widthStyle = (c: string): string | undefined => + resized && this.colWidths[c] != null ? `width:${this.colWidths[c]}px` : undefined; return html` -
- ${prov - ? html`` - : nothing} + `, + )} + ${formatCell(row[c])}
+
- ${hasProvenance ? html`` : nothing} + ${hasProvenance + ? html`` + : nothing} ${parsed.columns.map( - (c) => html``, )} diff --git a/chronos-vscode/webview/styles.css b/chronos-vscode/webview/styles.css index 9d40f5b..c803dfc 100644 --- a/chronos-vscode/webview/styles.css +++ b/chronos-vscode/webview/styles.css @@ -2556,6 +2556,14 @@ chronos-data-viewer, font-variant-numeric: tabular-nums; } +/* Once the user resizes a column, widths become authoritative: fixed layout + honors them exactly and the table grows to its content (the .dv-scroll + container then provides horizontal scrolling). */ +.dv-table.is-resized { + table-layout: fixed; + width: max-content; +} + .dv-table th, .dv-table td { text-align: left; @@ -2578,6 +2586,48 @@ chronos-data-viewer, white-space: nowrap; } +/* When widths are fixed, clip overflowing headers so a narrow column ellipsizes + its label instead of spilling over the next one. */ +.dv-table.is-resized thead th { + overflow: hidden; +} + +.dv-col-label { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Right-edge grab zone. Sits over the cell's right padding; a hairline appears + on hover/drag. The th is sticky (already a positioning context), so this + absolutely-positioned handle anchors to it. */ +.dv-resize { + position: absolute; + top: 0; + right: 0; + width: 8px; + height: 100%; + cursor: col-resize; + z-index: 2; +} + +.dv-resize::after { + content: ""; + position: absolute; + top: 18%; + bottom: 18%; + right: 3px; + width: 2px; + border-radius: 1px; + background: transparent; + transition: background 120ms ease; +} + +.dv-resize:hover::after { + background: var(--ch-border); +} + .dv-table thead th:hover { color: var(--ch-fg); } From ca5b5ac0954e70cf795576a5292c45f9373325c2 Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Thu, 25 Jun 2026 12:54:41 +0200 Subject: [PATCH 14/17] feat(extension): auto-start agent in Chronos workspaces; machine-scope dev settings Auto-open the panel and start a session when a Chronos workspace (a folder with .chronos/) is opened. Opt out with chronos.autoStartSession. Gated and hardened so the launch is never a surprise: - Fire only when the bootstrap is already reconciled (deps present AND at the ref this extension wants), not merely deps-present. Otherwise an extension upgrade (stale stored ref) would auto-fire an unsolicited `pi install` terminal on every startup, re-firing on each launch if the install failed. A stale / un-set-up workspace falls back to the manual command / walkthrough. - Serialise chronos.startSession with an in-flight guard so auto-start racing a walkthrough/manual click can't construct two panels + pi subprocesses before ChronosPanel.current is assigned. Mark chronos.piPath / piPackageSource / piNpmPackage as "scope": "machine" so a malicious workspace .vscode/settings.json can't supply values that get interpolated into the setup ShellExecution (the install path is reachable without a modal for an already-installed returning user). Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/package.json | 8 ++++ chronos-vscode/src/extension.ts | 81 ++++++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/chronos-vscode/package.json b/chronos-vscode/package.json index c29b11f..d37c56e 100644 --- a/chronos-vscode/package.json +++ b/chronos-vscode/package.json @@ -57,16 +57,24 @@ "chronos.piPath": { "type": "string", "default": "", + "scope": "machine", "description": "Path to the pi binary. Leave empty to use \"pi\" from PATH." }, + "chronos.autoStartSession": { + "type": "boolean", + "default": true, + "description": "Automatically open the Chronos panel and start an agent session when a Chronos workspace (a folder containing .chronos/) is opened. Only auto-starts when pi and the Chronos pi-package are already installed; otherwise start it manually with \"Chronos: Start Agent Session\"." + }, "chronos.piPackageSource": { "type": "string", "default": "", + "scope": "machine", "markdownDescription": "**Dev override.** Source the setup flow installs the Chronos pi-package from. Accepts a local path (`/abs/path/to/chronos/chronos`), a git URL, or a git URL with a branch/tag using the `@` form (`https://github.com/ai-historian/chronos@my-branch`). Leave empty to use the GitHub release pinned to this extension's version." }, "chronos.piNpmPackage": { "type": "string", "default": "", + "scope": "machine", "markdownDescription": "**Dev override.** npm package the setup flow installs for the `pi` CLI when pi isn't found. Leave empty for the default `@earendil-works/pi-coding-agent` (the maintained package). Set to override it — e.g. a fork or a version-pinned tag like `@earendil-works/pi-coding-agent@0.79.10`." } } diff --git a/chronos-vscode/src/extension.ts b/chronos-vscode/src/extension.ts index d50d0d8..1590d3e 100644 --- a/chronos-vscode/src/extension.ts +++ b/chronos-vscode/src/extension.ts @@ -77,6 +77,26 @@ function hasLocalChronosCheckout(): boolean { return piPackageSources().some((s) => isChronosEntry(s) && !isGitUrlEntry(s)); } +// True when pi + the pi-package are present AND already at the ref this extension +// wants — i.e. ensureBootstrap would early-return true without running any +// install/reinstall task. Auto-start is gated on this (not just deps-present) so +// an unsolicited launch never fires a (re)install terminal — e.g. after an +// extension upgrade leaves the stored ref stale, or an install previously failed. +// A stale / un-set-up workspace falls back to the manual command / walkthrough. +function bootstrapReconciled(context: vscode.ExtensionContext): boolean { + if (!hasPi() || !hasChronosPiPackage()) return false; + const cfg = vscode.workspace.getConfiguration("chronos"); + const sourceOverride = cfg.get("piPackageSource")?.trim(); + if (!sourceOverride && hasLocalChronosCheckout()) return true; + const wantedId = sourceOverride || `v${context.extension.packageJSON.version}`; + return context.globalState.get("chronos.piPackageRef") === wantedId; +} + +// Serialises chronos.startSession so a concurrent invocation (auto-start racing a +// walkthrough/manual click) joins the in-flight start instead of constructing a +// second panel + pi subprocess before ChronosPanel.current is assigned. +let startingSession = false; + // Run a shell command as a VS Code task and resolve with its exit code. A task // runs in the user's shell — login-shell PATH, sudo prompts, visible output, just // like the integrated terminal — but unlike terminal.sendText it reports @@ -707,27 +727,35 @@ export function activate(context: vscode.ExtensionContext): { return; } - // ensureBootstrap runs any install as an awaited task and returns true only - // once deps are present, so a false result means the user declined or the - // install failed (it already surfaced the error) — just stop. - if (!(await ensureBootstrap(context))) return; - refreshWalkthroughContext(workspaceFolder); + // A start is already in flight (e.g. auto-start on activation); it will + // create or reveal the panel, so don't run a second bootstrap + createOrShow. + if (startingSession) return; + startingSession = true; + try { + // ensureBootstrap runs any install as an awaited task and returns true only + // once deps are present, so a false result means the user declined or the + // install failed (it already surfaced the error) — just stop. + if (!(await ensureBootstrap(context))) return; + refreshWalkthroughContext(workspaceFolder); - const workspaceEnv = readEnvFile(join(workspaceFolder, ".chronos", ".env")); + const workspaceEnv = readEnvFile(join(workspaceFolder, ".chronos", ".env")); - // Bind the HTTP server (lazily) so the port is assigned before the agent starts. - await httpServer.start(); + // Bind the HTTP server (lazily) so the port is assigned before the agent starts. + await httpServer.start(); - const agentEnv = { - CHRONOS_HTTP_PORT: String(httpServer.port), - // pi >= 0.7x dropped the session_directory extension hook; this env var - // keeps session transcripts inside the workspace in both UI modes. - PI_CODING_AGENT_SESSION_DIR: join(workspaceFolder, "sessions"), - ...workspaceEnv, - }; + const agentEnv = { + CHRONOS_HTTP_PORT: String(httpServer.port), + // pi >= 0.7x dropped the session_directory extension hook; this env var + // keeps session transcripts inside the workspace in both UI modes. + PI_CODING_AGENT_SESSION_DIR: join(workspaceFolder, "sessions"), + ...workspaceEnv, + }; - // pi runs as an RPC subprocess behind the combined viewer + chat panel. - await ChronosPanel.createOrShow(context.extensionUri, workspaceFolder, agentEnv); + // pi runs as an RPC subprocess behind the combined viewer + chat panel. + await ChronosPanel.createOrShow(context.extensionUri, workspaceFolder, agentEnv); + } finally { + startingSession = false; + } }), vscode.commands.registerCommand("chronos.showPage", async () => { @@ -932,6 +960,25 @@ export function activate(context: vscode.ExtensionContext): { void promptRecoverIncompleteImports(workspaceFolder); } + // Auto-start the agent session when this folder is a Chronos workspace (it has + // a .chronos/ dir — the same signal that marks the workspace "initialized"). + // Gated on the bootstrap already being reconciled (deps present AND at the + // wanted ref) so an unsolicited launch never triggers an install/reinstall + // task — an un-set-up or just-upgraded workspace falls back to the manual + // command / walkthrough. Skipped under the integration test, which starts the + // session itself. Opt out with chronos.autoStartSession. + const autoStart = vscode.workspace.getConfiguration("chronos").get("autoStartSession", true); + if ( + process.env.CHRONOS_SKIP_BOOTSTRAP !== "1" && + autoStart && + workspaceFolder && + existsSync(join(workspaceFolder, ".chronos")) && + !ChronosPanel.active && + bootstrapReconciled(context) + ) { + void vscode.commands.executeCommand("chronos.startSession"); + } + return { getChronosStatus: () => ChronosPanel.getStatus(), // Test-only seam used by the integration suite to drive the webview. From 58d86e403847bcbb703b71830fe90ca892bee4ec Mon Sep 17 00:00:00 2001 From: Lorenz Hufe Date: Thu, 25 Jun 2026 12:54:50 +0200 Subject: [PATCH 15/17] fix(extension): credential file modes, login copy, drag capture, session label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-release review fixes around the new login flow and UI: - Write .chronos/.env and ~/.pi/agent/auth.json owner-only (0o600) and chmod after writing (writeFileSync's mode only applies on creation), and create the agent dir 0o700 — mirroring pi's auth-storage hardening. These hold billing-bearing API keys / long-lived OAuth tokens. - Use pointer capture on the main app splitter (same leak class as the data-viewer handles). - Generalise the login button/banner copy: it claimed "saves an API key to .chronos/.env", wrong for the Anthropic subscription OAuth path (which writes auth.json, no API key). - Skip the synthetic "Source selected: …" follow-up when picking a session's first-message label, so it never surfaces as the history label (the agent-side namer already skips it). Co-Authored-By: Claude Opus 4.8 (1M context) --- chronos-vscode/src/panel/chronos-panel.ts | 22 ++++++++++++++++--- chronos-vscode/src/panel/sessions.ts | 8 ++++++- .../webview/components/chronos-app.ts | 18 ++++++++++----- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/chronos-vscode/src/panel/chronos-panel.ts b/chronos-vscode/src/panel/chronos-panel.ts index 08d6360..6280c3b 100644 --- a/chronos-vscode/src/panel/chronos-panel.ts +++ b/chronos-vscode/src/panel/chronos-panel.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; import { resolvePiBin } from "../pi-env"; -import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, chmodSync } from "node:fs"; import { homedir } from "node:os"; import { join, dirname, basename, isAbsolute } from "node:path"; import { PiRpcSession } from "../rpc/pi-rpc-session"; @@ -56,7 +56,15 @@ function upsertEnvVar(envPath: string, key: string, value: string): void { : []; while (kept.length && kept[kept.length - 1].trim() === "") kept.pop(); kept.push(`${key}=${value}`); - writeFileSync(envPath, kept.join("\n") + "\n", "utf-8"); + // The .env holds billing-bearing provider API keys — keep it owner-only. + // writeFileSync's mode only applies on creation, so chmod too to tighten a + // pre-existing (possibly group/world-readable) file. + writeFileSync(envPath, kept.join("\n") + "\n", { encoding: "utf-8", mode: 0o600 }); + try { + chmodSync(envPath, 0o600); + } catch { + /* best-effort (e.g. unsupported fs) */ + } } // Write an Anthropic OAuth credential into pi's global auth store @@ -74,8 +82,16 @@ function writeAnthropicOAuth(creds: AnthropicOAuthCredentials): void { } } store.anthropic = { type: "oauth", refresh: creds.refresh, access: creds.access, expires: creds.expires }; - mkdirSync(dirname(authPath), { recursive: true }); + // Mirror pi's auth-storage hardening: owner-only dir + file. writeFileSync's + // mode only applies on creation, so chmod too in case auth.json pre-exists + // with looser permissions (it holds long-lived OAuth refresh/access tokens). + mkdirSync(dirname(authPath), { recursive: true, mode: 0o700 }); writeFileSync(authPath, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 }); + try { + chmodSync(authPath, 0o600); + } catch { + /* best-effort */ + } } // pi auto-registers every .md in a package's prompts/ dir as a slash command, diff --git a/chronos-vscode/src/panel/sessions.ts b/chronos-vscode/src/panel/sessions.ts index f801e7d..85944c2 100644 --- a/chronos-vscode/src/panel/sessions.ts +++ b/chronos-vscode/src/panel/sessions.ts @@ -67,7 +67,13 @@ function parseSessionFile(filePath: string): ChronosSessionInfo | undefined { if (entry.message?.role === "user") { messageCount++; if (!firstUserMessage) { - firstUserMessage = firstTextOf(entry.message?.content)?.slice(0, 200); + const text = firstTextOf(entry.message?.content); + // Skip the synthetic "Source selected: …" follow-up that /select-source + // injects, so it never becomes the session label (the agent-side namer + // skips it too; this keeps the degraded fallback meaningful). + if (text && !text.startsWith("Source selected:")) { + firstUserMessage = text.slice(0, 200); + } } } } catch { diff --git a/chronos-vscode/webview/components/chronos-app.ts b/chronos-vscode/webview/components/chronos-app.ts index efa96fb..7b677fb 100644 --- a/chronos-vscode/webview/components/chronos-app.ts +++ b/chronos-vscode/webview/components/chronos-app.ts @@ -313,17 +313,23 @@ export class ChronosApp extends LitElement { e.preventDefault(); const root = this.querySelector("#app-main"); if (!root) return; + // Capture on the splitter so a pointerup released outside the webview still + // tears the listeners down (a missed window pointerup would leak onMove). + const handle = e.currentTarget as HTMLElement; + handle.setPointerCapture(e.pointerId); const onMove = (ev: PointerEvent) => { const rect = root.getBoundingClientRect(); this.splitPct = Math.max(25, Math.min(75, ((ev.clientX - rect.left) / rect.width) * 100)); }; const onUp = () => { - window.removeEventListener("pointermove", onMove); - window.removeEventListener("pointerup", onUp); + handle.removeEventListener("pointermove", onMove); + handle.removeEventListener("pointerup", onUp); + handle.removeEventListener("lostpointercapture", onUp); this.dispatchEvent(new CustomEvent("ui-state-changed", { bubbles: true })); }; - window.addEventListener("pointermove", onMove); - window.addEventListener("pointerup", onUp); + handle.addEventListener("pointermove", onMove); + handle.addEventListener("pointerup", onUp); + handle.addEventListener("lostpointercapture", onUp); // also covers pointercancel } // ── render ──────────────────────────────────────────────────────────────── @@ -412,7 +418,7 @@ export class ChronosApp extends LitElement { ${this.renderContextMeter()}
this.toggleSort(c)} class=${this.sortColumn === c ? "is-sorted" : ""}> - ${c}${this.sortColumn === c ? html`${this.sortDir === 1 ? "▲" : "▼"}` : nothing} + (c) => html` this.toggleSort(c)} + class=${this.sortColumn === c ? "is-sorted" : ""} + > + ${c}${this.sortColumn === c ? html`${this.sortDir === 1 ? "▲" : "▼"}` : nothing} + this.onColResizeDown(e, c)} + @dblclick=${(e: MouseEvent) => this.onColResizeDblClick(e, c)} + @click=${(e: Event) => e.stopPropagation()} + >