From e05b656220a68656398ae798a4311f6a59adf5f8 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 17:58:37 +0200 Subject: [PATCH 01/19] scaffold project with pnpm, posthog-node, and opencode plugin types --- .gitignore | 4 ++ package.json | 35 ++++++++++++++ pnpm-lock.yaml | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 15 ++++++ 4 files changed, 175 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fb5861 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tsbuildinfo +.context/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..a66abcd --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "opencode-posthog", + "version": "0.1.0", + "description": "PostHog LLM Analytics plugin for OpenCode", + "type": "module", + "license": "MIT", + "main": "src/index.ts", + "keywords": [ + "opencode-plugin", + "posthog", + "ai", + "llm", + "observability", + "tracing", + "analytics" + ], + "packageManager": "pnpm@10.12.1", + "files": [ + "src", + "!src/**/*.test.ts" + ], + "dependencies": { + "posthog-node": "^5.0.0" + }, + "peerDependencies": { + "@opencode-ai/plugin": "*" + }, + "devDependencies": { + "@opencode-ai/plugin": "*", + "typescript": "^5.8.0" + }, + "scripts": { + "typecheck": "tsc --noEmit" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ffb5872 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,121 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + posthog-node: + specifier: ^5.0.0 + version: 5.28.11 + devDependencies: + '@opencode-ai/plugin': + specifier: '*' + version: 1.3.15 + typescript: + specifier: ^5.8.0 + version: 5.9.3 + +packages: + + '@opencode-ai/plugin@1.3.15': + resolution: {integrity: sha512-jZJbuvUXc5Limz8pacQl+ffATjjKGlq+xaA4wTUeW+/spwOf7Yr5Ryyvan8eNlYM8wy6h5SLfznl1rlFpjYC8w==} + peerDependencies: + '@opentui/core': '>=0.1.96' + '@opentui/solid': '>=0.1.96' + peerDependenciesMeta: + '@opentui/core': + optional: true + '@opentui/solid': + optional: true + + '@opencode-ai/sdk@1.3.15': + resolution: {integrity: sha512-Uk59C7wsK20wpdr277yx7Xz7TqG5jGqlZUpSW3wDH/7a2K2iBg0lXc2wskHuCXLRXMhXpPZtb4a3SOpPENkkbg==} + + '@posthog/core@1.24.6': + resolution: {integrity: sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + posthog-node@5.28.11: + resolution: {integrity: sha512-H4FOiqKUBO8SVXyXlU5tyifeS11hyTGVwBirFPR5rPtw8X6OFs5xVLx38YL7ZBLjaa9u8is+nIWXKBwWsZ2vlw==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + zod@4.1.8: + resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} + +snapshots: + + '@opencode-ai/plugin@1.3.15': + dependencies: + '@opencode-ai/sdk': 1.3.15 + zod: 4.1.8 + + '@opencode-ai/sdk@1.3.15': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/core@1.24.6': {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + isexe@2.0.0: {} + + path-key@3.1.1: {} + + posthog-node@5.28.11: + dependencies: + '@posthog/core': 1.24.6 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + typescript@5.9.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + zod@4.1.8: {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..427fd43 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "declaration": true, + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["src"] +} From e66f72781c640f4b6a86232bc1285f284e907fa0 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:01:19 +0200 Subject: [PATCH 02/19] add types, utilities, and version module --- src/types.ts | 33 ++++++++++++++ src/utils.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ src/version.ts | 1 + 3 files changed, 151 insertions(+) create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 src/version.ts diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..838bb04 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,33 @@ +export interface PluginConfig { + apiKey: string + host: string + privacyMode: boolean + enabled: boolean + distinctId: string + projectName: string + tags: Record + maxAttributeLength: number +} + +export interface TraceState { + traceId: string + sessionId: string + startTime: number + totalInputTokens: number + totalOutputTokens: number + totalCost: number + hadError: boolean + lastError?: string + userPrompt?: string + lastAssistantText?: string + currentAssistantMsg?: AssistantInfo + currentGenerationSpanId?: string + agentName?: string +} + +export interface AssistantInfo { + messageID: string + modelID: string + providerID: string + error?: { name: string; data?: Record } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..1eeaeff --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,117 @@ +import { hostname } from "node:os" +import { basename } from "node:path" +import type { PluginConfig } from "./types.js" + +export function loadConfig(): PluginConfig { + const tags: Record = {} + const tagsEnv = process.env.POSTHOG_TAGS + if (tagsEnv) { + for (const pair of tagsEnv.split(",")) { + const colonIdx = pair.indexOf(":") + if (colonIdx > 0) { + const key = pair.slice(0, colonIdx).trim() + const val = pair.slice(colonIdx + 1).trim() + if (key.length > 0 && val.length > 0) { + tags[key] = val + } + } + } + } + + let distinctId = process.env.POSTHOG_DISTINCT_ID + if (!distinctId) { + try { + distinctId = hostname() + } catch { + distinctId = "opencode-user" + } + } + + return { + apiKey: process.env.POSTHOG_API_KEY ?? "", + host: process.env.POSTHOG_HOST ?? "https://us.i.posthog.com", + privacyMode: process.env.POSTHOG_PRIVACY_MODE === "true", + enabled: process.env.POSTHOG_ENABLED !== "false", + distinctId, + projectName: + process.env.POSTHOG_PROJECT_NAME || basename(process.cwd()) || "opencode-project", + tags, + maxAttributeLength: + parseInt(process.env.POSTHOG_MAX_ATTRIBUTE_LENGTH ?? "12000", 10) || 12000, + } +} + +const SENSITIVE_KEY_PATTERN = + /api[-_]?key|token|secret|password|authorization|credential|private[-_]?key/i + +function redactSensitive( + value: unknown, + seen: WeakSet, + depth: number, +): unknown { + if (depth > 8) return "[DepthLimit]" + if (value === null || value === undefined) return value + if (typeof value !== "object") return value + if (seen.has(value)) return "[Circular]" + seen.add(value) + + if (Array.isArray(value)) { + return value.map((item) => redactSensitive(item, seen, depth + 1)) + } + + const output: Record = {} + for (const [key, nested] of Object.entries(value)) { + if (SENSITIVE_KEY_PATTERN.test(key)) { + output[key] = "[REDACTED]" + } else { + output[key] = redactSensitive(nested, seen, depth + 1) + } + } + return output +} + +function truncate(value: string, maxLength: number): string { + if (maxLength <= 0) return "" + if (value.length <= maxLength) return value + const omitted = value.length - maxLength + return `${value.slice(0, maxLength)}...[truncated ${omitted} chars]` +} + +export function serializeAttribute( + value: unknown, + maxLength: number, +): string | null { + if (value === undefined || value === null) return null + + const redacted = redactSensitive(value, new WeakSet(), 0) + + if (typeof redacted === "string") { + return truncate(redacted, maxLength) + } + + try { + const json = JSON.stringify(redacted) + if (json === undefined) return null + return truncate(json, maxLength) + } catch { + return "[Unserializable]" + } +} + +export function redactForPrivacy( + value: T, + privacyMode: boolean, +): T | null { + return privacyMode ? null : value +} + +export function serializeError( + error: { name: string; data?: Record } | undefined, +): string | null { + if (!error) return null + try { + return JSON.stringify(error) + } catch { + return error.name + } +} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..8500465 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const VERSION = "0.1.0" From 1fca5a9a86864a22c2cdfbb846e5cf57086c4d12 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:01:53 +0200 Subject: [PATCH 03/19] add event builders for generation, span, and trace --- src/events.ts | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/events.ts diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..6016469 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,168 @@ +import { randomUUID } from "node:crypto" +import type { StepFinishPart, ToolStateCompleted, ToolStateError } from "@opencode-ai/sdk" +import type { AssistantInfo, PluginConfig, TraceState } from "./types.js" +import { redactForPrivacy, serializeAttribute, serializeError } from "./utils.js" +import { VERSION } from "./version.js" + +export interface CaptureEvent { + event: string + distinctId: string + properties: Record +} + +const STOP_REASON_MAP: Record = { + stop: "stop", + length: "length", + "tool-calls": "tool_calls", + error: "error", +} + +function mapStopReason(reason: string | undefined): string | null { + if (!reason) return null + return STOP_REASON_MAP[reason] ?? reason +} + +export function buildAiGeneration( + part: StepFinishPart, + assistantInfo: AssistantInfo | undefined, + trace: TraceState, + config: PluginConfig, +): CaptureEvent { + const spanId = randomUUID() + trace.currentGenerationSpanId = spanId + + const inputMessages = redactForPrivacy( + trace.userPrompt ? [{ role: "user", content: trace.userPrompt }] : null, + config.privacyMode, + ) + + const outputChoices = redactForPrivacy( + trace.lastAssistantText + ? [{ role: "assistant", content: trace.lastAssistantText }] + : null, + config.privacyMode, + ) + + return { + event: "$ai_generation", + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_span_id: spanId, + $ai_model: assistantInfo?.modelID ?? "unknown", + $ai_provider: assistantInfo?.providerID ?? "unknown", + + $ai_input_tokens: part.tokens.input, + $ai_output_tokens: part.tokens.output, + $ai_reasoning_tokens: part.tokens.reasoning, + $ai_cache_read_input_tokens: part.tokens.cache.read, + $ai_cache_creation_input_tokens: part.tokens.cache.write, + + $ai_total_cost_usd: part.cost, + $ai_stop_reason: mapStopReason(part.reason), + + $ai_input: inputMessages, + $ai_output_choices: outputChoices, + + $ai_is_error: assistantInfo?.error ? true : false, + $ai_error: serializeError(assistantInfo?.error), + + $ai_lib: "opencode-posthog", + $ai_lib_version: VERSION, + $ai_framework: "opencode", + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, + } +} + +export function buildAiSpan( + toolName: string, + toolState: ToolStateCompleted | ToolStateError, + trace: TraceState, + config: PluginConfig, +): CaptureEvent { + const spanId = randomUUID() + const latency = (toolState.time.end - toolState.time.start) / 1000 + const isError = toolState.status === "error" + + const inputState = redactForPrivacy( + serializeAttribute(toolState.input, config.maxAttributeLength), + config.privacyMode, + ) + + let outputState: string | null = null + if (!config.privacyMode) { + if (toolState.status === "completed") { + outputState = serializeAttribute(toolState.output, config.maxAttributeLength) + } else { + outputState = serializeAttribute(toolState.error, config.maxAttributeLength) + } + } + + return { + event: "$ai_span", + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_span_id: spanId, + $ai_parent_id: trace.currentGenerationSpanId ?? null, + $ai_span_name: toolName, + + $ai_latency: latency, + + $ai_input_state: inputState, + $ai_output_state: outputState, + + $ai_is_error: isError, + $ai_error: isError ? (toolState as ToolStateError).error : null, + + $ai_lib: "opencode-posthog", + $ai_lib_version: VERSION, + $ai_framework: "opencode", + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, + } +} + +export function buildAiTrace( + trace: TraceState, + config: PluginConfig, +): CaptureEvent { + const latency = (Date.now() - trace.startTime) / 1000 + + return { + event: "$ai_trace", + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_latency: latency, + $ai_span_name: config.projectName, + + $ai_input_state: redactForPrivacy(trace.userPrompt ?? null, config.privacyMode), + $ai_output_state: redactForPrivacy( + trace.lastAssistantText ?? null, + config.privacyMode, + ), + + $ai_total_input_tokens: trace.totalInputTokens, + $ai_total_output_tokens: trace.totalOutputTokens, + + $ai_is_error: trace.hadError, + $ai_error: trace.lastError ?? null, + + $ai_lib: "opencode-posthog", + $ai_lib_version: VERSION, + $ai_framework: "opencode", + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, + } +} From b191247e1816b50fad5bce6f13aa274b894fef79 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:03:05 +0200 Subject: [PATCH 04/19] add plugin entry point with event router and state management --- package.json | 2 + pnpm-lock.yaml | 18 ++++ src/index.ts | 251 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 src/index.ts diff --git a/package.json b/package.json index a66abcd..20290d2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ }, "devDependencies": { "@opencode-ai/plugin": "*", + "@opencode-ai/sdk": "*", + "@types/node": "^22.0.0", "typescript": "^5.8.0" }, "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffb5872..ca45eec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,12 @@ importers: '@opencode-ai/plugin': specifier: '*' version: 1.3.15 + '@opencode-ai/sdk': + specifier: '*' + version: 1.3.15 + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 typescript: specifier: ^5.8.0 version: 5.9.3 @@ -38,6 +44,9 @@ packages: '@posthog/core@1.24.6': resolution: {integrity: sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ==} + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -71,6 +80,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -92,6 +104,10 @@ snapshots: '@posthog/core@1.24.6': {} + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -114,6 +130,8 @@ snapshots: typescript@5.9.3: {} + undici-types@6.21.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6f59e35 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,251 @@ +import type { Plugin } from "@opencode-ai/plugin" +import type { + Event, + AssistantMessage, + StepFinishPart, + ToolPart, + TextPart, + ToolStateCompleted, + ToolStateError, +} from "@opencode-ai/sdk" +import { randomUUID } from "node:crypto" +import { loadConfig } from "./utils.js" +import { buildAiGeneration, buildAiSpan, buildAiTrace } from "./events.js" +import type { AssistantInfo, TraceState } from "./types.js" +import type { CaptureEvent } from "./events.js" + +export const PostHogPlugin: Plugin = async () => { + const config = loadConfig() + + if (!config.enabled || !config.apiKey) return {} + + let client: import("posthog-node").PostHog | null = null + + async function ensureClient(): Promise { + if (client) return client + try { + const { PostHog } = await import("posthog-node") + client = new PostHog(config.apiKey, { + host: config.host, + flushAt: 20, + flushInterval: 10_000, + }) + return client + } catch { + return null + } + } + + function safeCapture(phClient: import("posthog-node").PostHog, event: CaptureEvent) { + try { + phClient.capture({ + distinctId: event.distinctId, + event: event.event, + properties: event.properties, + }) + } catch { + // never crash the host + } + } + + // State: sessionID -> trace state + const traces = new Map() + // State: messageID -> role for correlating parts to messages + const messageRoles = new Map() + // State: messageID -> assistant info + const assistantMessages = new Map() + + function getOrCreateTrace(sessionId: string): TraceState { + let trace = traces.get(sessionId) + if (!trace) { + trace = { + traceId: randomUUID(), + sessionId, + startTime: Date.now(), + totalInputTokens: 0, + totalOutputTokens: 0, + totalCost: 0, + hadError: false, + } + traces.set(sessionId, trace) + } + return trace + } + + function handleMessageUpdated(event: Event) { + if (event.type !== "message.updated") return + const msg = event.properties.info + + if (msg.role === "user") { + // New user message → new trace + const trace: TraceState = { + traceId: randomUUID(), + sessionId: msg.sessionID, + startTime: msg.time.created, + totalInputTokens: 0, + totalOutputTokens: 0, + totalCost: 0, + hadError: false, + agentName: msg.agent, + } + traces.set(msg.sessionID, trace) + messageRoles.set(msg.id, "user") + } else if (msg.role === "assistant") { + const assistant = msg as AssistantMessage + messageRoles.set(assistant.id, "assistant") + + const info: AssistantInfo = { + messageID: assistant.id, + modelID: assistant.modelID, + providerID: assistant.providerID, + error: assistant.error, + } + assistantMessages.set(assistant.id, info) + + // Update trace with current assistant info + const trace = getOrCreateTrace(assistant.sessionID) + trace.currentAssistantMsg = info + if (assistant.error) { + trace.hadError = true + trace.lastError = JSON.stringify(assistant.error) + } + } + } + + async function handlePartUpdated(event: Event) { + if (event.type !== "message.part.updated") return + const part = event.properties.part + + switch (part.type) { + case "text": + handleTextPart(part) + break + case "step-finish": + await handleStepFinish(part) + break + case "tool": + await handleToolPart(part) + break + } + } + + function handleTextPart(part: TextPart) { + const role = messageRoles.get(part.messageID) + if (!role) return + + const trace = traces.get(part.sessionID) + if (!trace) return + + if (role === "user") { + trace.userPrompt = part.text + } else if (role === "assistant") { + trace.lastAssistantText = part.text + } + } + + async function handleStepFinish(part: StepFinishPart) { + const phClient = await ensureClient() + if (!phClient) return + + const trace = traces.get(part.sessionID) + if (!trace) return + + const assistantInfo = trace.currentAssistantMsg + + // Accumulate tokens and cost + trace.totalInputTokens += part.tokens.input + trace.totalOutputTokens += part.tokens.output + trace.totalCost += part.cost + + const generation = buildAiGeneration(part, assistantInfo, trace, config) + safeCapture(phClient, generation) + } + + async function handleToolPart(part: ToolPart) { + if (part.state.status !== "completed" && part.state.status !== "error") return + + const phClient = await ensureClient() + if (!phClient) return + + const trace = traces.get(part.sessionID) + if (!trace) return + + const toolState = part.state as ToolStateCompleted | ToolStateError + const span = buildAiSpan(part.tool, toolState, trace, config) + safeCapture(phClient, span) + + if (part.state.status === "error") { + trace.hadError = true + trace.lastError = (part.state as ToolStateError).error + } + } + + async function handleSessionIdle(event: Event) { + if (event.type !== "session.idle") return + + const phClient = await ensureClient() + if (!phClient) return + + const sessionId = event.properties.sessionID + const trace = traces.get(sessionId) + if (!trace) return + + const traceEvent = buildAiTrace(trace, config) + safeCapture(phClient, traceEvent) + + try { + await phClient.flush() + } catch { + // ignore flush errors + } + + // Clean up state for this trace cycle + traces.delete(sessionId) + // Clean up message roles for this session + for (const [msgId, _] of messageRoles) { + // We can't efficiently filter by session, so we leave them + // They'll be overwritten on next use and are lightweight + } + } + + async function handleSessionError(event: Event) { + if (event.type !== "session.error") return + + const sessionId = event.properties.sessionID + if (!sessionId) return + + const trace = traces.get(sessionId) + if (trace) { + trace.hadError = true + if (event.properties.error) { + trace.lastError = JSON.stringify(event.properties.error) + } + } + } + + // Initialize client eagerly + await ensureClient() + + return { + event: async ({ event }) => { + try { + switch (event.type) { + case "message.updated": + handleMessageUpdated(event) + break + case "message.part.updated": + await handlePartUpdated(event) + break + case "session.idle": + await handleSessionIdle(event) + break + case "session.error": + await handleSessionError(event) + break + } + } catch { + // never crash OpenCode + } + }, + } +} From 5cf9ca87122b711d20f3acbc1adf8762048e71b4 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:03:44 +0200 Subject: [PATCH 05/19] add README with configuration, events, and privacy docs --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fbaa87..d71c075 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# opencode-posthog \ No newline at end of file +# opencode-posthog + +PostHog LLM Analytics plugin for [OpenCode](https://opencode.ai). Captures LLM generations, tool executions, and conversation traces, sending them to PostHog as structured `$ai_*` events for the LLM Analytics dashboard. + +## Installation + +Add to your `opencode.json`: + +```json +{ + "plugin": ["opencode-posthog"] +} +``` + +Or for local development, copy `src/index.ts` to `.opencode/plugins/posthog.ts`. + +## Configuration + +All configuration is via environment variables: + +| Variable | Default | Description | +|---|---|---| +| `POSTHOG_API_KEY` | _(required)_ | PostHog project API key | +| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog instance URL | +| `POSTHOG_PRIVACY_MODE` | `false` | Redact all LLM input/output content when `true` | +| `POSTHOG_ENABLED` | `true` | Set `false` to disable | +| `POSTHOG_DISTINCT_ID` | machine hostname | The `distinct_id` for all events | +| `POSTHOG_PROJECT_NAME` | cwd basename | Project name in all events | +| `POSTHOG_TAGS` | _(none)_ | Custom tags: `key1:val1,key2:val2` | +| `POSTHOG_MAX_ATTRIBUTE_LENGTH` | `12000` | Max length for serialized tool input/output | + +If `POSTHOG_API_KEY` is not set, the plugin is a no-op. + +## Events + +### `$ai_generation` — per LLM call + +Emitted for each LLM roundtrip (step-finish part). Properties include: + +- `$ai_model`, `$ai_provider` — model and provider identifiers +- `$ai_input_tokens`, `$ai_output_tokens`, `$ai_reasoning_tokens` — token counts +- `$ai_cache_read_input_tokens`, `$ai_cache_creation_input_tokens` — cache token counts +- `$ai_total_cost_usd` — cost in USD +- `$ai_latency` — not available per-step (use trace-level latency) +- `$ai_stop_reason` — `stop`, `tool_calls`, `error`, etc. +- `$ai_input`, `$ai_output_choices` — message content (null in privacy mode) +- `$ai_trace_id`, `$ai_span_id`, `$ai_session_id` — correlation IDs + +### `$ai_span` — per tool execution + +Emitted when a tool call completes or errors. Properties include: + +- `$ai_span_name` — tool name (`read`, `write`, `bash`, `edit`, etc.) +- `$ai_latency` — execution time in seconds +- `$ai_input_state`, `$ai_output_state` — tool input/output (null in privacy mode) +- `$ai_parent_id` — span ID of the generation that triggered this tool +- `$ai_is_error`, `$ai_error` — error status + +### `$ai_trace` — per user prompt + +Emitted on `session.idle` (agent finished responding). Properties include: + +- `$ai_trace_id`, `$ai_session_id` — correlation IDs +- `$ai_latency` — total trace time in seconds +- `$ai_total_input_tokens`, `$ai_total_output_tokens` — accumulated token counts +- `$ai_input_state`, `$ai_output_state` — user prompt and final response +- `$ai_is_error` — whether any step/tool errored + +## Privacy + +When `POSTHOG_PRIVACY_MODE=true`, all content fields (`$ai_input`, `$ai_output_choices`, `$ai_input_state`, `$ai_output_state`) are set to `null`. Token counts, costs, latency, and model metadata still flow. + +Sensitive keys (matching `api_key`, `token`, `secret`, `password`, `authorization`, `credential`, `private_key`) are always redacted in tool inputs/outputs regardless of privacy mode. + +## License + +MIT From feca4791b938c635afadce312299bb2fc1d584fc Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:04:08 +0200 Subject: [PATCH 06/19] remove dead loop in session idle cleanup --- src/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6f59e35..2a3ae51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,13 +199,7 @@ export const PostHogPlugin: Plugin = async () => { // ignore flush errors } - // Clean up state for this trace cycle traces.delete(sessionId) - // Clean up message roles for this session - for (const [msgId, _] of messageRoles) { - // We can't efficiently filter by session, so we leave them - // They'll be overwritten on next use and are lightweight - } } async function handleSessionError(event: Event) { From edfc0bedc4407ae0f749f60ce9a50cf4f4d7893a Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 18:53:32 +0200 Subject: [PATCH 07/19] fix span parenting and string-level secret redaction Allocate generation span ID on step-start instead of step-finish so tool spans emitted during the step reference the correct parent. Scan plain strings for sensitive key-value patterns (JSON fragments, key=value) since tool output arrives as a string and was previously passed through unredacted. --- src/events.ts | 5 +++-- src/index.ts | 12 ++++++++++++ src/utils.ts | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/events.ts b/src/events.ts index 6016469..6dc7fef 100644 --- a/src/events.ts +++ b/src/events.ts @@ -28,8 +28,9 @@ export function buildAiGeneration( trace: TraceState, config: PluginConfig, ): CaptureEvent { - const spanId = randomUUID() - trace.currentGenerationSpanId = spanId + // Use the span ID allocated at step-start so tool spans emitted + // during this step already reference the correct parent. + const spanId = trace.currentGenerationSpanId ?? randomUUID() const inputMessages = redactForPrivacy( trace.userPrompt ? [{ role: "user", content: trace.userPrompt }] : null, diff --git a/src/index.ts b/src/index.ts index 2a3ae51..767521c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin" import type { Event, AssistantMessage, + StepStartPart, StepFinishPart, ToolPart, TextPart, @@ -120,6 +121,9 @@ export const PostHogPlugin: Plugin = async () => { case "text": handleTextPart(part) break + case "step-start": + handleStepStart(part) + break case "step-finish": await handleStepFinish(part) break @@ -143,6 +147,14 @@ export const PostHogPlugin: Plugin = async () => { } } + function handleStepStart(part: StepStartPart) { + const trace = traces.get(part.sessionID) + if (!trace) return + // Allocate the generation span ID eagerly so that tool spans + // emitted during this step can reference it as their parent. + trace.currentGenerationSpanId = randomUUID() + } + async function handleStepFinish(part: StepFinishPart) { const phClient = await ensureClient() if (!phClient) return diff --git a/src/utils.ts b/src/utils.ts index 1eeaeff..0f7b797 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,6 +44,21 @@ export function loadConfig(): PluginConfig { const SENSITIVE_KEY_PATTERN = /api[-_]?key|token|secret|password|authorization|credential|private[-_]?key/i +/** + * Pattern to detect sensitive values embedded in strings, e.g. JSON + * fragments like `"api_key":"sk-..."` or `password=secret` in command output. + */ +const SENSITIVE_VALUE_PATTERN = + /(?:"(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)"\s*:\s*"[^"]*"|(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)\s*[=:]\s*\S+)/gi + +/** + * Redact sensitive values found inline in a string. Handles both JSON-like + * `"key":"value"` patterns and `key=value` / `key: value` patterns. + */ +function redactStringValues(str: string): string { + return str.replace(SENSITIVE_VALUE_PATTERN, "[REDACTED]") +} + function redactSensitive( value: unknown, seen: WeakSet, @@ -51,6 +66,7 @@ function redactSensitive( ): unknown { if (depth > 8) return "[DepthLimit]" if (value === null || value === undefined) return value + if (typeof value === "string") return redactStringValues(value) if (typeof value !== "object") return value if (seen.has(value)) return "[Circular]" seen.add(value) From a9003b55c5b069e4d1489f784d236de6a85ba65b Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:04:18 +0200 Subject: [PATCH 08/19] add vitest tests for event builders and utilities Covers mapStopReason, buildAiGeneration, buildAiSpan, buildAiTrace, redactForPrivacy, serializeAttribute (including string-level redaction), and serializeError. Tests privacy mode, custom tags, error handling, pre-allocated span IDs, truncation, circular refs, and deep nesting. --- package.json | 5 +- pnpm-lock.yaml | 972 +++++++++++++++++++++++++++++++++++++++++++++ src/events.test.ts | 314 +++++++++++++++ src/events.ts | 2 +- src/utils.test.ts | 115 ++++++ 5 files changed, 1406 insertions(+), 2 deletions(-) create mode 100644 src/events.test.ts create mode 100644 src/utils.test.ts diff --git a/package.json b/package.json index 20290d2..4510533 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,12 @@ "@opencode-ai/plugin": "*", "@opencode-ai/sdk": "*", "@types/node": "^22.0.0", - "typescript": "^5.8.0" + "typescript": "^5.8.0", + "vitest": "^3.0.0" }, "scripts": { + "test": "vitest run", + "test:watch": "vitest", "typecheck": "tsc --noEmit" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca45eec..c8189bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,9 +24,171 @@ importers: typescript: specifier: ^5.8.0 version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@22.19.17) packages: + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@opencode-ai/plugin@1.3.15': resolution: {integrity: sha512-jZJbuvUXc5Limz8pacQl+ffATjjKGlq+xaA4wTUeW+/spwOf7Yr5Ryyvan8eNlYM8wy6h5SLfznl1rlFpjYC8w==} peerDependencies: @@ -44,20 +206,276 @@ packages: '@posthog/core@1.24.6': resolution: {integrity: sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ==} + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + posthog-node@5.28.11: resolution: {integrity: sha512-H4FOiqKUBO8SVXyXlU5tyifeS11hyTGVwBirFPR5rPtw8X6OFs5xVLx38YL7ZBLjaa9u8is+nIWXKBwWsZ2vlw==} engines: {node: ^20.20.0 || >=22.22.0} @@ -67,6 +485,11 @@ packages: rxjs: optional: true + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -75,6 +498,44 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -83,16 +544,174 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + zod@4.1.8: resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} snapshots: + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + '@opencode-ai/plugin@1.3.15': dependencies: '@opencode-ai/sdk': 1.3.15 @@ -104,36 +723,389 @@ snapshots: '@posthog/core@1.24.6': {} + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.17))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.17) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + isexe@2.0.0: {} + js-tokens@9.0.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + path-key@3.1.1: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + posthog-node@5.28.11: dependencies: '@posthog/core': 1.24.6 + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + typescript@5.9.3: {} undici-types@6.21.0: {} + vite-node@3.2.4(@types/node@22.19.17): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.17) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@22.19.17): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.17 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@22.19.17): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.17)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.17) + vite-node: 3.2.4(@types/node@22.19.17) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + zod@4.1.8: {} diff --git a/src/events.test.ts b/src/events.test.ts new file mode 100644 index 0000000..a0f2d7b --- /dev/null +++ b/src/events.test.ts @@ -0,0 +1,314 @@ +import { describe, it, expect } from "vitest" +import { buildAiGeneration, buildAiSpan, buildAiTrace, mapStopReason } from "./events.js" +import type { PluginConfig, TraceState, AssistantInfo } from "./types.js" +import type { StepFinishPart, ToolStateCompleted, ToolStateError } from "@opencode-ai/sdk" + +const defaultConfig: PluginConfig = { + apiKey: "test-key", + host: "https://us.i.posthog.com", + privacyMode: false, + enabled: true, + distinctId: "test-host", + projectName: "my-project", + tags: {}, + maxAttributeLength: 12000, +} + +const privacyConfig: PluginConfig = { + ...defaultConfig, + privacyMode: true, +} + +const configWithTags: PluginConfig = { + ...defaultConfig, + tags: { team: "platform", env: "staging" }, +} + +function makeStepFinish(overrides?: Partial): StepFinishPart { + return { + id: "part-1", + sessionID: "session-1", + messageID: "msg-1", + type: "step-finish", + reason: "stop", + cost: 0.003, + tokens: { + input: 100, + output: 50, + reasoning: 10, + cache: { read: 5, write: 3 }, + }, + ...overrides, + } +} + +function makeTrace(overrides?: Partial): TraceState { + return { + traceId: "trace-123", + sessionId: "session-1", + startTime: Date.now() - 5000, + totalInputTokens: 500, + totalOutputTokens: 200, + totalCost: 0.01, + hadError: false, + agentName: "my-project", + currentGenerationSpanId: "gen-span-1", + userPrompt: "Hello", + lastAssistantText: "Hi there!", + ...overrides, + } +} + +function makeAssistantInfo(overrides?: Partial): AssistantInfo { + return { + messageID: "msg-1", + modelID: "claude-sonnet-4-20250514", + providerID: "anthropic", + ...overrides, + } +} + +describe("mapStopReason", () => { + it("maps known stop reasons", () => { + expect(mapStopReason("stop")).toBe("stop") + expect(mapStopReason("length")).toBe("length") + expect(mapStopReason("tool-calls")).toBe("tool_calls") + expect(mapStopReason("error")).toBe("error") + }) + + it("returns null for undefined", () => { + expect(mapStopReason(undefined)).toBeNull() + }) + + it("passes through unknown reasons", () => { + expect(mapStopReason("custom_reason")).toBe("custom_reason") + }) +}) + +describe("buildAiGeneration", () => { + it("builds generation event with all fields", () => { + const part = makeStepFinish() + const trace = makeTrace() + const assistant = makeAssistantInfo() + + const result = buildAiGeneration(part, assistant, trace, defaultConfig) + + expect(result.event).toBe("$ai_generation") + expect(result.distinctId).toBe("test-host") + expect(result.properties.$ai_model).toBe("claude-sonnet-4-20250514") + expect(result.properties.$ai_provider).toBe("anthropic") + expect(result.properties.$ai_input_tokens).toBe(100) + expect(result.properties.$ai_output_tokens).toBe(50) + expect(result.properties.$ai_reasoning_tokens).toBe(10) + expect(result.properties.$ai_cache_read_input_tokens).toBe(5) + expect(result.properties.$ai_cache_creation_input_tokens).toBe(3) + expect(result.properties.$ai_total_cost_usd).toBe(0.003) + expect(result.properties.$ai_stop_reason).toBe("stop") + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_trace_id).toBe("trace-123") + expect(result.properties.$ai_span_id).toBe("gen-span-1") + expect(result.properties.$ai_session_id).toBe("session-1") + expect(result.properties.$ai_lib).toBe("opencode-posthog") + expect(result.properties.$ai_framework).toBe("opencode") + expect(result.properties.$ai_project_name).toBe("my-project") + expect(result.properties.$ai_agent_name).toBe("my-project") + }) + + it("uses pre-allocated span ID from trace", () => { + const trace = makeTrace({ currentGenerationSpanId: "pre-allocated-id" }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_span_id).toBe("pre-allocated-id") + }) + + it("falls back to random UUID when no pre-allocated span ID", () => { + const trace = makeTrace({ currentGenerationSpanId: undefined }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_span_id).toBeDefined() + expect(typeof result.properties.$ai_span_id).toBe("string") + }) + + it("includes input and output content", () => { + const trace = makeTrace({ + userPrompt: "What is 2+2?", + lastAssistantText: "4", + }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_input).toEqual([{ role: "user", content: "What is 2+2?" }]) + expect(result.properties.$ai_output_choices).toEqual([{ role: "assistant", content: "4" }]) + }) + + it("redacts content in privacy mode", () => { + const trace = makeTrace() + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, privacyConfig) + expect(result.properties.$ai_input).toBeNull() + expect(result.properties.$ai_output_choices).toBeNull() + }) + + it("marks error generations", () => { + const assistant = makeAssistantInfo({ + error: { name: "UnknownError", data: { message: "Rate limited" } }, + }) + const result = buildAiGeneration(makeStepFinish(), assistant, makeTrace(), defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toContain("Rate limited") + }) + + it("falls back to unknown model when no assistant info", () => { + const result = buildAiGeneration(makeStepFinish(), undefined, makeTrace(), defaultConfig) + expect(result.properties.$ai_model).toBe("unknown") + expect(result.properties.$ai_provider).toBe("unknown") + }) + + it("includes custom tags", () => { + const result = buildAiGeneration( + makeStepFinish(), + makeAssistantInfo(), + makeTrace(), + configWithTags, + ) + expect(result.properties.team).toBe("platform") + expect(result.properties.env).toBe("staging") + }) +}) + +describe("buildAiSpan", () => { + const completedState: ToolStateCompleted = { + status: "completed", + input: { command: "ls -la" }, + output: "file1.txt\nfile2.txt", + title: "bash", + metadata: {}, + time: { start: 1000, end: 1250 }, + } + + const errorState: ToolStateError = { + status: "error", + input: { command: "bad-cmd" }, + error: "command not found", + time: { start: 1000, end: 1050 }, + } + + it("builds span event for completed tool", () => { + const trace = makeTrace() + const result = buildAiSpan("bash", completedState, trace, defaultConfig) + + expect(result.event).toBe("$ai_span") + expect(result.distinctId).toBe("test-host") + expect(result.properties.$ai_trace_id).toBe("trace-123") + expect(result.properties.$ai_parent_id).toBe("gen-span-1") + expect(result.properties.$ai_span_name).toBe("bash") + expect(result.properties.$ai_latency).toBe(0.25) + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_error).toBeNull() + expect(result.properties.$ai_input_state).toBe('{"command":"ls -la"}') + expect(result.properties.$ai_output_state).toBe("file1.txt\nfile2.txt") + expect(result.properties.$ai_lib).toBe("opencode-posthog") + expect(result.properties.$ai_framework).toBe("opencode") + }) + + it("uses currentGenerationSpanId as parent", () => { + const trace = makeTrace({ currentGenerationSpanId: "parent-gen-42" }) + const result = buildAiSpan("read", completedState, trace, defaultConfig) + expect(result.properties.$ai_parent_id).toBe("parent-gen-42") + }) + + it("sets parent to null when no generation span ID", () => { + const trace = makeTrace({ currentGenerationSpanId: undefined }) + const result = buildAiSpan("read", completedState, trace, defaultConfig) + expect(result.properties.$ai_parent_id).toBeNull() + }) + + it("redacts tool input/output in privacy mode", () => { + const result = buildAiSpan("read", completedState, makeTrace(), privacyConfig) + expect(result.properties.$ai_input_state).toBeNull() + expect(result.properties.$ai_output_state).toBeNull() + }) + + it("redacts sensitive keys in tool input", () => { + const stateWithSecrets: ToolStateCompleted = { + ...completedState, + input: { + command: "curl", + api_key: "sk-secret-123", + headers: { authorization: "Bearer tok" }, + }, + } + const result = buildAiSpan("bash", stateWithSecrets, makeTrace(), defaultConfig) + const inputState = result.properties.$ai_input_state as string + expect(inputState).toContain("[REDACTED]") + expect(inputState).not.toContain("sk-secret-123") + expect(inputState).not.toContain("Bearer tok") + expect(inputState).toContain("curl") + }) + + it("captures error info", () => { + const result = buildAiSpan("bash", errorState, makeTrace(), defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toBe("command not found") + expect(result.properties.$ai_latency).toBe(0.05) + }) + + it("includes custom tags", () => { + const result = buildAiSpan("read", completedState, makeTrace(), configWithTags) + expect(result.properties.team).toBe("platform") + expect(result.properties.env).toBe("staging") + }) +}) + +describe("buildAiTrace", () => { + it("builds trace event with accumulated totals", () => { + const trace = makeTrace() + const result = buildAiTrace(trace, defaultConfig) + + expect(result.event).toBe("$ai_trace") + expect(result.distinctId).toBe("test-host") + expect(result.properties.$ai_trace_id).toBe("trace-123") + expect(result.properties.$ai_session_id).toBe("session-1") + expect(result.properties.$ai_latency).toBeGreaterThan(0) + expect(result.properties.$ai_total_input_tokens).toBe(500) + expect(result.properties.$ai_total_output_tokens).toBe(200) + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_span_name).toBe("my-project") + expect(result.properties.$ai_lib).toBe("opencode-posthog") + expect(result.properties.$ai_framework).toBe("opencode") + }) + + it("includes user prompt and assistant text", () => { + const trace = makeTrace({ + userPrompt: "Explain X", + lastAssistantText: "X is...", + }) + const result = buildAiTrace(trace, defaultConfig) + expect(result.properties.$ai_input_state).toBe("Explain X") + expect(result.properties.$ai_output_state).toBe("X is...") + }) + + it("redacts content in privacy mode", () => { + const trace = makeTrace({ + userPrompt: "secret prompt", + lastAssistantText: "secret response", + }) + const result = buildAiTrace(trace, privacyConfig) + expect(result.properties.$ai_input_state).toBeNull() + expect(result.properties.$ai_output_state).toBeNull() + // Metrics still flow + expect(result.properties.$ai_total_input_tokens).toBe(500) + expect(result.properties.$ai_total_output_tokens).toBe(200) + }) + + it("captures error traces", () => { + const trace = makeTrace({ + hadError: true, + lastError: "Context overflow", + }) + const result = buildAiTrace(trace, defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toBe("Context overflow") + }) + + it("includes custom tags", () => { + const result = buildAiTrace(makeTrace(), configWithTags) + expect(result.properties.team).toBe("platform") + expect(result.properties.env).toBe("staging") + }) +}) diff --git a/src/events.ts b/src/events.ts index 6dc7fef..c2e6580 100644 --- a/src/events.ts +++ b/src/events.ts @@ -17,7 +17,7 @@ const STOP_REASON_MAP: Record = { error: "error", } -function mapStopReason(reason: string | undefined): string | null { +export function mapStopReason(reason: string | undefined): string | null { if (!reason) return null return STOP_REASON_MAP[reason] ?? reason } diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..d17ab1a --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect } from "vitest" +import { redactForPrivacy, serializeAttribute, serializeError } from "./utils.js" + +describe("redactForPrivacy", () => { + it("returns value when privacy mode is off", () => { + expect(redactForPrivacy("hello", false)).toBe("hello") + expect(redactForPrivacy({ key: "val" }, false)).toEqual({ key: "val" }) + }) + + it("returns null when privacy mode is on", () => { + expect(redactForPrivacy("hello", true)).toBeNull() + expect(redactForPrivacy({ key: "val" }, true)).toBeNull() + }) +}) + +describe("serializeAttribute", () => { + it("serializes simple values", () => { + expect(serializeAttribute({ a: 1 }, 1000)).toBe('{"a":1}') + expect(serializeAttribute("hello", 1000)).toBe("hello") + }) + + it("redacts sensitive keys in objects", () => { + const input = { + command: "curl", + api_key: "sk-secret-123", + apiKey: "another-secret", + token: "my-token", + password: "pass123", + authorization: "Bearer xyz", + normal_field: "visible", + } + const result = serializeAttribute(input, 10000) + expect(result).toContain("[REDACTED]") + expect(result).not.toContain("sk-secret-123") + expect(result).not.toContain("another-secret") + expect(result).not.toContain("my-token") + expect(result).not.toContain("pass123") + expect(result).not.toContain("Bearer xyz") + expect(result).toContain("curl") + expect(result).toContain("visible") + }) + + it("redacts nested sensitive keys", () => { + const input = { + headers: { Authorization: "Bearer secret" }, + config: { api_key: "hidden" }, + } + const result = serializeAttribute(input, 10000) + expect(result).not.toContain("secret") + expect(result).not.toContain("hidden") + }) + + it("redacts sensitive values embedded in strings", () => { + const jsonStr = '{"api_key":"sk-secret-123","name":"test"}' + const result = serializeAttribute(jsonStr, 10000) + expect(result).not.toContain("sk-secret-123") + expect(result).toContain("[REDACTED]") + }) + + it("redacts key=value patterns in strings", () => { + const cmdOutput = "config loaded: password=hunter2 host=localhost" + const result = serializeAttribute(cmdOutput, 10000) + expect(result).not.toContain("hunter2") + expect(result).toContain("[REDACTED]") + }) + + it("truncates long output", () => { + const longStr = "a".repeat(200) + const result = serializeAttribute(longStr, 50) + expect(result).not.toBeNull() + expect(result!.length).toBeLessThan(200) + expect(result).toContain("...[truncated") + }) + + it("handles circular references", () => { + const obj: Record = { name: "test" } + obj.self = obj + const result = serializeAttribute(obj, 1000) + expect(result).toContain("[Circular]") + expect(result).toContain("test") + }) + + it("handles deep nesting", () => { + let obj: Record = { value: "deep" } + for (let i = 0; i < 20; i++) { + obj = { nested: obj } + } + const result = serializeAttribute(obj, 10000) + expect(result).toContain("[DepthLimit]") + }) + + it("returns null for undefined and null", () => { + expect(serializeAttribute(undefined, 1000)).toBeNull() + expect(serializeAttribute(null, 1000)).toBeNull() + }) +}) + +describe("serializeError", () => { + it("serializes error objects to JSON", () => { + const error = { name: "UnknownError", data: { message: "boom" } } + const result = serializeError(error) + expect(result).toBe('{"name":"UnknownError","data":{"message":"boom"}}') + }) + + it("returns null for undefined", () => { + expect(serializeError(undefined)).toBeNull() + }) + + it("falls back to error name on serialization failure", () => { + const circular: Record = { name: "BadError" } + circular.self = circular + const result = serializeError(circular as { name: string; data?: Record }) + expect(result).toBe("BadError") + }) +}) From 60ab456fff12f8427bcd872e29479a1abd9463c0 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:18:14 +0200 Subject: [PATCH 09/19] fix bearer token redaction, error field leaks, and per-step input context 1. String redaction now consumes full multi-word values after sensitive keys (e.g. "Authorization: Bearer secret-token") instead of stopping at the first whitespace boundary. 2. All $ai_error fields and trace.lastError now route through serializeAttribute instead of raw JSON.stringify, so sensitive keys in error payloads are redacted before being sent to PostHog. 3. Generations now use per-step accumulated input messages (including tool results) rather than just the initial user prompt, making multi-step/tool-assisted analytics accurate. --- src/events.test.ts | 24 +++++++++++++++++++++++- src/events.ts | 18 ++++++++++++------ src/index.ts | 27 ++++++++++++++++++++++----- src/types.ts | 9 +++++++++ src/utils.test.ts | 29 +++++++++++++++++++++++++++-- src/utils.ts | 29 +++++++++++++++++++---------- 6 files changed, 112 insertions(+), 24 deletions(-) diff --git a/src/events.test.ts b/src/events.test.ts index a0f2d7b..7c998a1 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -55,6 +55,8 @@ function makeTrace(overrides?: Partial): TraceState { currentGenerationSpanId: "gen-span-1", userPrompt: "Hello", lastAssistantText: "Hi there!", + stepInputMessages: [{ role: "user", content: "Hello" }], + stepAssistantText: "Hi there!", ...overrides, } } @@ -127,9 +129,11 @@ describe("buildAiGeneration", () => { expect(typeof result.properties.$ai_span_id).toBe("string") }) - it("includes input and output content", () => { + it("includes input and output content from step messages", () => { const trace = makeTrace({ userPrompt: "What is 2+2?", + stepInputMessages: [{ role: "user", content: "What is 2+2?" }], + stepAssistantText: "4", lastAssistantText: "4", }) const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) @@ -137,6 +141,24 @@ describe("buildAiGeneration", () => { expect(result.properties.$ai_output_choices).toEqual([{ role: "assistant", content: "4" }]) }) + it("includes tool results in input for multi-step generations", () => { + const trace = makeTrace({ + stepInputMessages: [ + { role: "user", content: "Read the file" }, + { role: "tool", content: "[read] file contents here" }, + ], + stepAssistantText: "I read the file.", + }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_input).toEqual([ + { role: "user", content: "Read the file" }, + { role: "tool", content: "[read] file contents here" }, + ]) + expect(result.properties.$ai_output_choices).toEqual([ + { role: "assistant", content: "I read the file." }, + ]) + }) + it("redacts content in privacy mode", () => { const trace = makeTrace() const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, privacyConfig) diff --git a/src/events.ts b/src/events.ts index c2e6580..41f1fa5 100644 --- a/src/events.ts +++ b/src/events.ts @@ -32,14 +32,16 @@ export function buildAiGeneration( // during this step already reference the correct parent. const spanId = trace.currentGenerationSpanId ?? randomUUID() + // Use accumulated step input messages (includes user prompt + tool results) + // for accurate per-roundtrip context. const inputMessages = redactForPrivacy( - trace.userPrompt ? [{ role: "user", content: trace.userPrompt }] : null, + trace.stepInputMessages.length > 0 ? trace.stepInputMessages : null, config.privacyMode, ) const outputChoices = redactForPrivacy( - trace.lastAssistantText - ? [{ role: "assistant", content: trace.lastAssistantText }] + trace.stepAssistantText + ? [{ role: "assistant", content: trace.stepAssistantText }] : null, config.privacyMode, ) @@ -67,7 +69,7 @@ export function buildAiGeneration( $ai_output_choices: outputChoices, $ai_is_error: assistantInfo?.error ? true : false, - $ai_error: serializeError(assistantInfo?.error), + $ai_error: serializeError(assistantInfo?.error, config.maxAttributeLength), $ai_lib: "opencode-posthog", $ai_lib_version: VERSION, @@ -119,7 +121,9 @@ export function buildAiSpan( $ai_output_state: outputState, $ai_is_error: isError, - $ai_error: isError ? (toolState as ToolStateError).error : null, + $ai_error: isError + ? serializeAttribute((toolState as ToolStateError).error, config.maxAttributeLength) + : null, $ai_lib: "opencode-posthog", $ai_lib_version: VERSION, @@ -156,7 +160,9 @@ export function buildAiTrace( $ai_total_output_tokens: trace.totalOutputTokens, $ai_is_error: trace.hadError, - $ai_error: trace.lastError ?? null, + $ai_error: trace.lastError + ? serializeAttribute(trace.lastError, config.maxAttributeLength) + : null, $ai_lib: "opencode-posthog", $ai_lib_version: VERSION, diff --git a/src/index.ts b/src/index.ts index 767521c..5d66ce7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import type { ToolStateError, } from "@opencode-ai/sdk" import { randomUUID } from "node:crypto" -import { loadConfig } from "./utils.js" +import { loadConfig, serializeAttribute } from "./utils.js" import { buildAiGeneration, buildAiSpan, buildAiTrace } from "./events.js" import type { AssistantInfo, TraceState } from "./types.js" import type { CaptureEvent } from "./events.js" @@ -67,6 +67,7 @@ export const PostHogPlugin: Plugin = async () => { totalOutputTokens: 0, totalCost: 0, hadError: false, + stepInputMessages: [], } traces.set(sessionId, trace) } @@ -88,6 +89,7 @@ export const PostHogPlugin: Plugin = async () => { totalCost: 0, hadError: false, agentName: msg.agent, + stepInputMessages: [], } traces.set(msg.sessionID, trace) messageRoles.set(msg.id, "user") @@ -108,7 +110,7 @@ export const PostHogPlugin: Plugin = async () => { trace.currentAssistantMsg = info if (assistant.error) { trace.hadError = true - trace.lastError = JSON.stringify(assistant.error) + trace.lastError = serializeAttribute(assistant.error, config.maxAttributeLength) ?? assistant.error.name } } } @@ -142,8 +144,10 @@ export const PostHogPlugin: Plugin = async () => { if (role === "user") { trace.userPrompt = part.text + trace.stepInputMessages.push({ role: "user", content: part.text }) } else if (role === "assistant") { trace.lastAssistantText = part.text + trace.stepAssistantText = part.text } } @@ -153,6 +157,8 @@ export const PostHogPlugin: Plugin = async () => { // Allocate the generation span ID eagerly so that tool spans // emitted during this step can reference it as their parent. trace.currentGenerationSpanId = randomUUID() + // Reset per-step assistant text for the new generation + trace.stepAssistantText = undefined } async function handleStepFinish(part: StepFinishPart) { @@ -186,9 +192,20 @@ export const PostHogPlugin: Plugin = async () => { const span = buildAiSpan(part.tool, toolState, trace, config) safeCapture(phClient, span) - if (part.state.status === "error") { + // Feed tool result into step input so subsequent generations include + // the tool context the model actually saw. + if (toolState.status === "completed") { + trace.stepInputMessages.push({ + role: "tool", + content: `[${part.tool}] ${toolState.output}`, + }) + } else { + trace.stepInputMessages.push({ + role: "tool", + content: `[${part.tool}] ERROR: ${toolState.error}`, + }) trace.hadError = true - trace.lastError = (part.state as ToolStateError).error + trace.lastError = toolState.error } } @@ -224,7 +241,7 @@ export const PostHogPlugin: Plugin = async () => { if (trace) { trace.hadError = true if (event.properties.error) { - trace.lastError = JSON.stringify(event.properties.error) + trace.lastError = serializeAttribute(event.properties.error, config.maxAttributeLength) ?? "unknown error" } } } diff --git a/src/types.ts b/src/types.ts index 838bb04..7e54f3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,11 @@ export interface PluginConfig { maxAttributeLength: number } +export interface InputMessage { + role: string + content: string +} + export interface TraceState { traceId: string sessionId: string @@ -20,6 +25,10 @@ export interface TraceState { lastError?: string userPrompt?: string lastAssistantText?: string + /** Accumulated input context for the current step (user prompt + tool results). */ + stepInputMessages: InputMessage[] + /** Assistant text accumulated during the current step, reset on each step-start. */ + stepAssistantText?: string currentAssistantMsg?: AssistantInfo currentGenerationSpanId?: string agentName?: string diff --git a/src/utils.test.ts b/src/utils.test.ts index d17ab1a..b273611 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -64,6 +64,22 @@ describe("serializeAttribute", () => { expect(result).toContain("[REDACTED]") }) + it("redacts multi-word bearer token values", () => { + const header = "Authorization: Bearer secret-token" + const result = serializeAttribute(header, 10000) + expect(result).not.toContain("secret-token") + expect(result).not.toContain("Bearer") + expect(result).toContain("[REDACTED]") + }) + + it("redacts header-style secrets with colons in tool output", () => { + const output = "HTTP/1.1 200 OK\nAuthorization: Bearer sk-abc123\nContent-Type: text/plain" + const result = serializeAttribute(output, 10000) + expect(result).not.toContain("sk-abc123") + expect(result).toContain("[REDACTED]") + expect(result).toContain("Content-Type") + }) + it("truncates long output", () => { const longStr = "a".repeat(200) const result = serializeAttribute(longStr, 50) @@ -106,10 +122,19 @@ describe("serializeError", () => { expect(serializeError(undefined)).toBeNull() }) - it("falls back to error name on serialization failure", () => { + it("handles circular references via redaction", () => { const circular: Record = { name: "BadError" } circular.self = circular const result = serializeError(circular as { name: string; data?: Record }) - expect(result).toBe("BadError") + expect(result).toContain("BadError") + expect(result).toContain("[Circular]") + }) + + it("redacts sensitive keys in error data", () => { + const error = { name: "AuthError", data: { api_key: "sk-secret-123", message: "failed" } } + const result = serializeError(error) + expect(result).not.toContain("sk-secret-123") + expect(result).toContain("[REDACTED]") + expect(result).toContain("failed") }) }) diff --git a/src/utils.ts b/src/utils.ts index 0f7b797..dadf35d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,18 +45,30 @@ const SENSITIVE_KEY_PATTERN = /api[-_]?key|token|secret|password|authorization|credential|private[-_]?key/i /** - * Pattern to detect sensitive values embedded in strings, e.g. JSON - * fragments like `"api_key":"sk-..."` or `password=secret` in command output. + * Patterns to detect sensitive values embedded in strings. + * + * JSON-style: `"api_key": "sk-secret-123"` or `"token":"abc"` + * Matches the full `"key":"value"` including the quoted value. */ -const SENSITIVE_VALUE_PATTERN = - /(?:"(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)"\s*:\s*"[^"]*"|(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)\s*[=:]\s*\S+)/gi +const SENSITIVE_JSON_PATTERN = + /"(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)"\s*:\s*"[^"]*"/gi + +/** + * Header/env-style: `Authorization: Bearer secret-token` or `password=hunter2` + * Matches the key and everything to the next comma, semicolon, newline, or + * end-of-string so multi-word values like `Bearer xyz` are fully consumed. + */ +const SENSITIVE_KV_PATTERN = + /(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)\s*[=:]\s*[^\n,;]*/gi /** * Redact sensitive values found inline in a string. Handles both JSON-like * `"key":"value"` patterns and `key=value` / `key: value` patterns. */ function redactStringValues(str: string): string { - return str.replace(SENSITIVE_VALUE_PATTERN, "[REDACTED]") + return str + .replace(SENSITIVE_JSON_PATTERN, "[REDACTED]") + .replace(SENSITIVE_KV_PATTERN, "[REDACTED]") } function redactSensitive( @@ -123,11 +135,8 @@ export function redactForPrivacy( export function serializeError( error: { name: string; data?: Record } | undefined, + maxLength: number = 12000, ): string | null { if (!error) return null - try { - return JSON.stringify(error) - } catch { - return error.name - } + return serializeAttribute(error, maxLength) } From 5003b3f76f8337726b5d7842ce13ab2866079ece Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:21:53 +0200 Subject: [PATCH 10/19] use static import for posthog-node instead of dynamic import() OpenCode installs plugin dependencies at startup, so posthog-node is always available when the plugin runs. Replace the lazy ensureClient pattern with a straightforward static import and direct construction. --- src/index.ts | 62 +++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5d66ce7..f2b77b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import type { ToolStateCompleted, ToolStateError, } from "@opencode-ai/sdk" +import { PostHog } from "posthog-node" import { randomUUID } from "node:crypto" import { loadConfig, serializeAttribute } from "./utils.js" import { buildAiGeneration, buildAiSpan, buildAiTrace } from "./events.js" @@ -20,26 +21,15 @@ export const PostHogPlugin: Plugin = async () => { if (!config.enabled || !config.apiKey) return {} - let client: import("posthog-node").PostHog | null = null + const client = new PostHog(config.apiKey, { + host: config.host, + flushAt: 20, + flushInterval: 10_000, + }) - async function ensureClient(): Promise { - if (client) return client + function safeCapture(event: CaptureEvent) { try { - const { PostHog } = await import("posthog-node") - client = new PostHog(config.apiKey, { - host: config.host, - flushAt: 20, - flushInterval: 10_000, - }) - return client - } catch { - return null - } - } - - function safeCapture(phClient: import("posthog-node").PostHog, event: CaptureEvent) { - try { - phClient.capture({ + client.capture({ distinctId: event.distinctId, event: event.event, properties: event.properties, @@ -115,7 +105,7 @@ export const PostHogPlugin: Plugin = async () => { } } - async function handlePartUpdated(event: Event) { + function handlePartUpdated(event: Event) { if (event.type !== "message.part.updated") return const part = event.properties.part @@ -127,10 +117,10 @@ export const PostHogPlugin: Plugin = async () => { handleStepStart(part) break case "step-finish": - await handleStepFinish(part) + handleStepFinish(part) break case "tool": - await handleToolPart(part) + handleToolPart(part) break } } @@ -161,10 +151,7 @@ export const PostHogPlugin: Plugin = async () => { trace.stepAssistantText = undefined } - async function handleStepFinish(part: StepFinishPart) { - const phClient = await ensureClient() - if (!phClient) return - + function handleStepFinish(part: StepFinishPart) { const trace = traces.get(part.sessionID) if (!trace) return @@ -176,21 +163,18 @@ export const PostHogPlugin: Plugin = async () => { trace.totalCost += part.cost const generation = buildAiGeneration(part, assistantInfo, trace, config) - safeCapture(phClient, generation) + safeCapture(generation) } - async function handleToolPart(part: ToolPart) { + function handleToolPart(part: ToolPart) { if (part.state.status !== "completed" && part.state.status !== "error") return - const phClient = await ensureClient() - if (!phClient) return - const trace = traces.get(part.sessionID) if (!trace) return const toolState = part.state as ToolStateCompleted | ToolStateError const span = buildAiSpan(part.tool, toolState, trace, config) - safeCapture(phClient, span) + safeCapture(span) // Feed tool result into step input so subsequent generations include // the tool context the model actually saw. @@ -212,18 +196,15 @@ export const PostHogPlugin: Plugin = async () => { async function handleSessionIdle(event: Event) { if (event.type !== "session.idle") return - const phClient = await ensureClient() - if (!phClient) return - const sessionId = event.properties.sessionID const trace = traces.get(sessionId) if (!trace) return const traceEvent = buildAiTrace(trace, config) - safeCapture(phClient, traceEvent) + safeCapture(traceEvent) try { - await phClient.flush() + await client.flush() } catch { // ignore flush errors } @@ -231,7 +212,7 @@ export const PostHogPlugin: Plugin = async () => { traces.delete(sessionId) } - async function handleSessionError(event: Event) { + function handleSessionError(event: Event) { if (event.type !== "session.error") return const sessionId = event.properties.sessionID @@ -246,9 +227,6 @@ export const PostHogPlugin: Plugin = async () => { } } - // Initialize client eagerly - await ensureClient() - return { event: async ({ event }) => { try { @@ -257,13 +235,13 @@ export const PostHogPlugin: Plugin = async () => { handleMessageUpdated(event) break case "message.part.updated": - await handlePartUpdated(event) + handlePartUpdated(event) break case "session.idle": await handleSessionIdle(event) break case "session.error": - await handleSessionError(event) + handleSessionError(event) break } } catch { From 31057945801896b2e5011cc1170f8905e7c3ffd0 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:39:09 +0200 Subject: [PATCH 11/19] redact tool output in step input, clean up per-message state, fix README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Tool output stored in stepInputMessages now goes through serializeAttribute before being appended, so secrets in tool results are redacted in $ai_generation.$ai_input too. 2. Track message IDs per trace and delete entries from messageRoles and assistantMessages on session.idle, preventing unbounded memory growth in long-lived processes. 3. Fix local development instructions in README — the plugin has multiple source files, so copying just index.ts doesn't work. --- README.md | 2 +- src/events.test.ts | 1 + src/index.ts | 17 ++++++++++++++--- src/types.ts | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d71c075..8c10a19 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Add to your `opencode.json`: } ``` -Or for local development, copy `src/index.ts` to `.opencode/plugins/posthog.ts`. +For local development, clone or symlink this repo into `.opencode/plugins/` and ensure `posthog-node` is installed (add it to `.opencode/package.json`). ## Configuration diff --git a/src/events.test.ts b/src/events.test.ts index 7c998a1..648b1d9 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -57,6 +57,7 @@ function makeTrace(overrides?: Partial): TraceState { lastAssistantText: "Hi there!", stepInputMessages: [{ role: "user", content: "Hello" }], stepAssistantText: "Hi there!", + messageIds: new Set(), ...overrides, } } diff --git a/src/index.ts b/src/index.ts index f2b77b3..e8cf963 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,7 @@ export const PostHogPlugin: Plugin = async () => { totalCost: 0, hadError: false, stepInputMessages: [], + messageIds: new Set(), } traces.set(sessionId, trace) } @@ -80,6 +81,7 @@ export const PostHogPlugin: Plugin = async () => { hadError: false, agentName: msg.agent, stepInputMessages: [], + messageIds: new Set([msg.id]), } traces.set(msg.sessionID, trace) messageRoles.set(msg.id, "user") @@ -97,6 +99,7 @@ export const PostHogPlugin: Plugin = async () => { // Update trace with current assistant info const trace = getOrCreateTrace(assistant.sessionID) + trace.messageIds.add(assistant.id) trace.currentAssistantMsg = info if (assistant.error) { trace.hadError = true @@ -177,16 +180,19 @@ export const PostHogPlugin: Plugin = async () => { safeCapture(span) // Feed tool result into step input so subsequent generations include - // the tool context the model actually saw. + // the tool context the model actually saw. Redact and truncate to + // match the treatment applied to $ai_span fields. if (toolState.status === "completed") { + const redacted = serializeAttribute(toolState.output, config.maxAttributeLength) ?? "" trace.stepInputMessages.push({ role: "tool", - content: `[${part.tool}] ${toolState.output}`, + content: `[${part.tool}] ${redacted}`, }) } else { + const redacted = serializeAttribute(toolState.error, config.maxAttributeLength) ?? "" trace.stepInputMessages.push({ role: "tool", - content: `[${part.tool}] ERROR: ${toolState.error}`, + content: `[${part.tool}] ERROR: ${redacted}`, }) trace.hadError = true trace.lastError = toolState.error @@ -209,6 +215,11 @@ export const PostHogPlugin: Plugin = async () => { // ignore flush errors } + // Clean up per-message state for this trace + for (const msgId of trace.messageIds) { + messageRoles.delete(msgId) + assistantMessages.delete(msgId) + } traces.delete(sessionId) } diff --git a/src/types.ts b/src/types.ts index 7e54f3d..da201fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,8 @@ export interface TraceState { currentAssistantMsg?: AssistantInfo currentGenerationSpanId?: string agentName?: string + /** Message IDs belonging to this trace, for cleanup. */ + messageIds: Set } export interface AssistantInfo { From c0348b5856a08378b10376903b54934f0c8214ae Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:48:50 +0200 Subject: [PATCH 12/19] set version to 0.0.1, add author, CI workflow, and reorder package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Version 0.0.1, author Nejc Drobnič - Reorder package.json fields: identity → module config → files → keywords → scripts → dependencies - Add GitHub Actions CI workflow (typecheck + test) with setup action from posthog-pi, add .nvmrc for Node 22 --- .github/actions/setup/action.yml | 37 ++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 22 +++++++++++++++++++ .nvmrc | 1 + package.json | 25 ++++++++++----------- src/version.ts | 2 +- 5 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .nvmrc diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..41b930d --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,37 @@ +name: Setup +description: Setup Node.js and install dependencies + +inputs: + install: + description: 'Whether to install dependencies' + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Setup pnpm + uses: pnpm/action-setup@c336a2788d9774dccfdeb4823a5058ccc9f07453 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Cache dependencies + id: pnpm-cache + uses: actions/cache@v4 + with: + path: | + **/node_modules + .pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} + restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + ${{ runner.os }}-pnpm- + + - name: Install dependencies + if: inputs.install == 'true' + run: pnpm install --frozen-lockfile + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a4eca9b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup environment + uses: ./.github/actions/setup + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json index 4510533..37445c1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,16 @@ { "name": "opencode-posthog", - "version": "0.1.0", + "version": "0.0.1", "description": "PostHog LLM Analytics plugin for OpenCode", - "type": "module", + "author": "Nejc Drobnič ", "license": "MIT", + "type": "module", "main": "src/index.ts", + "packageManager": "pnpm@10.12.1", + "files": [ + "src", + "!src/**/*.test.ts" + ], "keywords": [ "opencode-plugin", "posthog", @@ -14,11 +20,11 @@ "tracing", "analytics" ], - "packageManager": "pnpm@10.12.1", - "files": [ - "src", - "!src/**/*.test.ts" - ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, "dependencies": { "posthog-node": "^5.0.0" }, @@ -31,10 +37,5 @@ "@types/node": "^22.0.0", "typescript": "^5.8.0", "vitest": "^3.0.0" - }, - "scripts": { - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit" } } diff --git a/src/version.ts b/src/version.ts index 8500465..2dc6a32 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.1.0" +export const VERSION = "0.0.1" From 700fc7db22e13574618659a51ae84a70df27b6f3 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:53:55 +0200 Subject: [PATCH 13/19] add oxlint/oxfmt, license headers, derive version from package.json - Add oxlint (lint) and oxfmt (format) with configs matching posthog-pi - Reformat all source files to single quotes, 4-space indent, 120 cols - Add MIT license headers to CI workflow and setup action - Derive VERSION from package.json instead of hardcoding it - Add lint step to CI workflow - Fix no-unneeded-ternary lint warning in events.ts --- .github/actions/setup/action.yml | 22 ++ .github/workflows/ci.yml | 25 ++ .oxfmtrc.json | 9 + .oxlintrc.json | 11 + README.md | 22 +- package.json | 82 +++-- pnpm-lock.yaml | 415 +++++++++++++++++++++ src/events.test.ts | 601 +++++++++++++++---------------- src/events.ts | 300 ++++++++------- src/index.ts | 477 ++++++++++++------------ src/types.ts | 66 ++-- src/utils.test.ts | 270 +++++++------- src/utils.ts | 175 ++++----- src/version.ts | 7 +- tsconfig.json | 26 +- 15 files changed, 1484 insertions(+), 1024 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 41b930d..a03d810 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,3 +1,25 @@ +# MIT License +# +# Copyright (c) 2026 PostHog Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + name: Setup description: Setup Node.js and install dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4eca9b..6c5f9e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,25 @@ +# MIT License +# +# Copyright (c) 2026 PostHog Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + name: CI on: @@ -18,5 +40,8 @@ jobs: - name: Typecheck run: pnpm typecheck + - name: Lint + run: pnpm lint + - name: Test run: pnpm test diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..6b73352 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "sortPackageJson": false, + "ignorePatterns": ["node_modules", "dist", "pnpm-lock.yaml"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6eb6e2d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "promise", "vitest"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "perf": "warn" + }, + "rules": {}, + "ignorePatterns": ["node_modules", "dist"] +} diff --git a/README.md b/README.md index 8c10a19..5037f09 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Add to your `opencode.json`: ```json { - "plugin": ["opencode-posthog"] + "plugin": ["opencode-posthog"] } ``` @@ -18,16 +18,16 @@ For local development, clone or symlink this repo into `.opencode/plugins/` and All configuration is via environment variables: -| Variable | Default | Description | -|---|---|---| -| `POSTHOG_API_KEY` | _(required)_ | PostHog project API key | -| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog instance URL | -| `POSTHOG_PRIVACY_MODE` | `false` | Redact all LLM input/output content when `true` | -| `POSTHOG_ENABLED` | `true` | Set `false` to disable | -| `POSTHOG_DISTINCT_ID` | machine hostname | The `distinct_id` for all events | -| `POSTHOG_PROJECT_NAME` | cwd basename | Project name in all events | -| `POSTHOG_TAGS` | _(none)_ | Custom tags: `key1:val1,key2:val2` | -| `POSTHOG_MAX_ATTRIBUTE_LENGTH` | `12000` | Max length for serialized tool input/output | +| Variable | Default | Description | +| ------------------------------ | -------------------------- | ----------------------------------------------- | +| `POSTHOG_API_KEY` | _(required)_ | PostHog project API key | +| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog instance URL | +| `POSTHOG_PRIVACY_MODE` | `false` | Redact all LLM input/output content when `true` | +| `POSTHOG_ENABLED` | `true` | Set `false` to disable | +| `POSTHOG_DISTINCT_ID` | machine hostname | The `distinct_id` for all events | +| `POSTHOG_PROJECT_NAME` | cwd basename | Project name in all events | +| `POSTHOG_TAGS` | _(none)_ | Custom tags: `key1:val1,key2:val2` | +| `POSTHOG_MAX_ATTRIBUTE_LENGTH` | `12000` | Max length for serialized tool input/output | If `POSTHOG_API_KEY` is not set, the plugin is a no-op. diff --git a/package.json b/package.json index 37445c1..e3305a5 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,45 @@ { - "name": "opencode-posthog", - "version": "0.0.1", - "description": "PostHog LLM Analytics plugin for OpenCode", - "author": "Nejc Drobnič ", - "license": "MIT", - "type": "module", - "main": "src/index.ts", - "packageManager": "pnpm@10.12.1", - "files": [ - "src", - "!src/**/*.test.ts" - ], - "keywords": [ - "opencode-plugin", - "posthog", - "ai", - "llm", - "observability", - "tracing", - "analytics" - ], - "scripts": { - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "posthog-node": "^5.0.0" - }, - "peerDependencies": { - "@opencode-ai/plugin": "*" - }, - "devDependencies": { - "@opencode-ai/plugin": "*", - "@opencode-ai/sdk": "*", - "@types/node": "^22.0.0", - "typescript": "^5.8.0", - "vitest": "^3.0.0" - } + "name": "opencode-posthog", + "version": "0.0.1", + "description": "PostHog LLM Analytics plugin for OpenCode", + "author": "Nejc Drobnič ", + "license": "MIT", + "type": "module", + "main": "src/index.ts", + "packageManager": "pnpm@10.12.1", + "files": [ + "src", + "!src/**/*.test.ts" + ], + "keywords": [ + "opencode-plugin", + "posthog", + "ai", + "llm", + "observability", + "tracing", + "analytics" + ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "lint": "oxlint src/ && oxfmt --check .", + "lint:fix": "oxlint --fix src/ && oxfmt --write ." + }, + "dependencies": { + "posthog-node": "^5.0.0" + }, + "peerDependencies": { + "@opencode-ai/plugin": "*" + }, + "devDependencies": { + "@opencode-ai/plugin": "*", + "@opencode-ai/sdk": "*", + "@types/node": "^22.0.0", + "oxfmt": "^0.40.0", + "oxlint": "^1.55.0", + "typescript": "^5.8.0", + "vitest": "^3.0.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8189bd..16120c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,12 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.17 + oxfmt: + specifier: ^0.40.0 + version: 0.40.0 + oxlint: + specifier: ^1.55.0 + version: 1.58.0 typescript: specifier: ^5.8.0 version: 5.9.3 @@ -203,6 +209,234 @@ packages: '@opencode-ai/sdk@1.3.15': resolution: {integrity: sha512-Uk59C7wsK20wpdr277yx7Xz7TqG5jGqlZUpSW3wDH/7a2K2iBg0lXc2wskHuCXLRXMhXpPZtb4a3SOpPENkkbg==} + '@oxfmt/binding-android-arm-eabi@0.40.0': + resolution: {integrity: sha512-S6zd5r1w/HmqR8t0CTnGjFTBLDq2QKORPwriCHxo4xFNuhmOTABGjPaNvCJJVnrKBLsohOeiDX3YqQfJPF+FXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.40.0': + resolution: {integrity: sha512-/mbS9UUP/5Vbl2D6osIdcYiP0oie63LKMoTyGj5hyMCK/SFkl3EhtyRAfdjPvuvHC0SXdW6ePaTKkBSq1SNcIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.40.0': + resolution: {integrity: sha512-wRt8fRdfLiEhnRMBonlIbKrJWixoEmn6KCjKE9PElnrSDSXETGZfPb8ee+nQNTobXkCVvVLytp2o0obAsxl78Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.40.0': + resolution: {integrity: sha512-fzowhqbOE/NRy+AE5ob0+Y4X243WbWzDb00W+pKwD7d9tOqsAFbtWUwIyqqCoCLxj791m2xXIEeLH/3uz7zCCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.40.0': + resolution: {integrity: sha512-agZ9ITaqdBjcerRRFEHB8s0OyVcQW8F9ZxsszjxzeSthQ4fcN2MuOtQFWec1ed8/lDa50jSLHVE2/xPmTgtCfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.40.0': + resolution: {integrity: sha512-ZM2oQ47p28TP1DVIp7HL1QoMUgqlBFHey0ksHct7tMXoU5BqjNvPWw7888azzMt25lnyPODVuye1wvNbvVUFOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.40.0': + resolution: {integrity: sha512-RBFPAxRAIsMisKM47Oe6Lwdv6agZYLz02CUhVCD1sOv5ajAcRMrnwCFBPWwGXpazToW2mjnZxFos8TuFjTU15A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.40.0': + resolution: {integrity: sha512-Nb2XbQ+wV3W2jSIihXdPj7k83eOxeSgYP3N/SRXvQ6ZYPIk6Q86qEh5Gl/7OitX3bQoQrESqm1yMLvZV8/J7dA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-arm64-musl@0.40.0': + resolution: {integrity: sha512-tGmWhLD/0YMotCdfezlT6tC/MJG/wKpo4vnQ3Cq+4eBk/BwNv7EmkD0VkD5F/dYkT3b8FNU01X2e8vvJuWoM1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-ppc64-gnu@0.40.0': + resolution: {integrity: sha512-rVbFyM3e7YhkVnp0IVYjaSHfrBWcTRWb60LEcdNAJcE2mbhTpbqKufx0FrhWfoxOrW/+7UJonAOShoFFLigDqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-gnu@0.40.0': + resolution: {integrity: sha512-3ZqBw14JtWeEoLiioJcXSJz8RQyPE+3jLARnYM1HdPzZG4vk+Ua8CUupt2+d+vSAvMyaQBTN2dZK+kbBS/j5mA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-musl@0.40.0': + resolution: {integrity: sha512-JJ4PPSdcbGBjPvb+O7xYm2FmAsKCyuEMYhqatBAHMp/6TA6rVlf9Z/sYPa4/3Bommb+8nndm15SPFRHEPU5qFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-s390x-gnu@0.40.0': + resolution: {integrity: sha512-Kp0zNJoX9Ik77wUya2tpBY3W9f40VUoMQLWVaob5SgCrblH/t2xr/9B2bWHfs0WCefuGmqXcB+t0Lq77sbBmZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxfmt/binding-linux-x64-gnu@0.40.0': + resolution: {integrity: sha512-7YTCNzleWTaQTqNGUNQ66qVjpoV6DjbCOea+RnpMBly2bpzrI/uu7Rr+2zcgRfNxyjXaFTVQKaRKjqVdeUfeVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-linux-x64-musl@0.40.0': + resolution: {integrity: sha512-hWnSzJ0oegeOwfOEeejYXfBqmnRGHusgtHfCPzmvJvHTwy1s3Neo59UKc1CmpE3zxvrCzJoVHos0rr97GHMNPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-openharmony-arm64@0.40.0': + resolution: {integrity: sha512-28sJC1lR4qtBJGzSRRbPnSW3GxU2+4YyQFE6rCmsUYqZ5XYH8jg0/w+CvEzQ8TuAQz5zLkcA25nFQGwoU0PT3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.40.0': + resolution: {integrity: sha512-cDkRnyT0dqwF5oIX1Cv59HKCeZQFbWWdUpXa3uvnHFT2iwYSSZspkhgjXjU6iDp5pFPaAEAe9FIbMoTgkTmKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.40.0': + resolution: {integrity: sha512-7rPemBJjqm5Gkv6ZRCPvK8lE6AqQ/2z31DRdWazyx2ZvaSgL7QGofHXHNouRpPvNsT9yxRNQJgigsWkc+0qg4w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.40.0': + resolution: {integrity: sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.58.0': + resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.58.0': + resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.58.0': + resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.58.0': + resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.58.0': + resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': + resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.58.0': + resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.58.0': + resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.58.0': + resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.58.0': + resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.58.0': + resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.58.0': + resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.58.0': + resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.58.0': + resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.58.0': + resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.58.0': + resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.58.0': + resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.58.0': + resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.58.0': + resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@posthog/core@1.24.6': resolution: {integrity: sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ==} @@ -454,6 +688,21 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + oxfmt@0.40.0: + resolution: {integrity: sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint@1.58.0: + resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.18.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -528,6 +777,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -721,6 +974,120 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@oxfmt/binding-android-arm-eabi@0.40.0': + optional: true + + '@oxfmt/binding-android-arm64@0.40.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.40.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.40.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.40.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.40.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.40.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.40.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.40.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.40.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.40.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.40.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.40.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.40.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.40.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.40.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.40.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.40.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.40.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.58.0': + optional: true + + '@oxlint/binding-android-arm64@1.58.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.58.0': + optional: true + + '@oxlint/binding-darwin-x64@1.58.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.58.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.58.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.58.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.58.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.58.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.58.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.58.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.58.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.58.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.58.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.58.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.58.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.58.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.58.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.58.0': + optional: true + '@posthog/core@1.24.6': {} '@rollup/rollup-android-arm-eabi@4.60.1': @@ -937,6 +1304,52 @@ snapshots: nanoid@3.3.11: {} + oxfmt@0.40.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.40.0 + '@oxfmt/binding-android-arm64': 0.40.0 + '@oxfmt/binding-darwin-arm64': 0.40.0 + '@oxfmt/binding-darwin-x64': 0.40.0 + '@oxfmt/binding-freebsd-x64': 0.40.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.40.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.40.0 + '@oxfmt/binding-linux-arm64-gnu': 0.40.0 + '@oxfmt/binding-linux-arm64-musl': 0.40.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.40.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.40.0 + '@oxfmt/binding-linux-riscv64-musl': 0.40.0 + '@oxfmt/binding-linux-s390x-gnu': 0.40.0 + '@oxfmt/binding-linux-x64-gnu': 0.40.0 + '@oxfmt/binding-linux-x64-musl': 0.40.0 + '@oxfmt/binding-openharmony-arm64': 0.40.0 + '@oxfmt/binding-win32-arm64-msvc': 0.40.0 + '@oxfmt/binding-win32-ia32-msvc': 0.40.0 + '@oxfmt/binding-win32-x64-msvc': 0.40.0 + + oxlint@1.58.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.58.0 + '@oxlint/binding-android-arm64': 1.58.0 + '@oxlint/binding-darwin-arm64': 1.58.0 + '@oxlint/binding-darwin-x64': 1.58.0 + '@oxlint/binding-freebsd-x64': 1.58.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.58.0 + '@oxlint/binding-linux-arm-musleabihf': 1.58.0 + '@oxlint/binding-linux-arm64-gnu': 1.58.0 + '@oxlint/binding-linux-arm64-musl': 1.58.0 + '@oxlint/binding-linux-ppc64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-gnu': 1.58.0 + '@oxlint/binding-linux-riscv64-musl': 1.58.0 + '@oxlint/binding-linux-s390x-gnu': 1.58.0 + '@oxlint/binding-linux-x64-gnu': 1.58.0 + '@oxlint/binding-linux-x64-musl': 1.58.0 + '@oxlint/binding-openharmony-arm64': 1.58.0 + '@oxlint/binding-win32-arm64-msvc': 1.58.0 + '@oxlint/binding-win32-ia32-msvc': 1.58.0 + '@oxlint/binding-win32-x64-msvc': 1.58.0 + path-key@3.1.1: {} pathe@2.0.3: {} @@ -1017,6 +1430,8 @@ snapshots: tinypool@1.1.1: {} + tinypool@2.1.0: {} + tinyrainbow@2.0.0: {} tinyspy@4.0.4: {} diff --git a/src/events.test.ts b/src/events.test.ts index 648b1d9..9535ea2 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -1,337 +1,330 @@ -import { describe, it, expect } from "vitest" -import { buildAiGeneration, buildAiSpan, buildAiTrace, mapStopReason } from "./events.js" -import type { PluginConfig, TraceState, AssistantInfo } from "./types.js" -import type { StepFinishPart, ToolStateCompleted, ToolStateError } from "@opencode-ai/sdk" +import { describe, it, expect } from 'vitest' +import { buildAiGeneration, buildAiSpan, buildAiTrace, mapStopReason } from './events.js' +import type { PluginConfig, TraceState, AssistantInfo } from './types.js' +import type { StepFinishPart, ToolStateCompleted, ToolStateError } from '@opencode-ai/sdk' const defaultConfig: PluginConfig = { - apiKey: "test-key", - host: "https://us.i.posthog.com", - privacyMode: false, - enabled: true, - distinctId: "test-host", - projectName: "my-project", - tags: {}, - maxAttributeLength: 12000, + apiKey: 'test-key', + host: 'https://us.i.posthog.com', + privacyMode: false, + enabled: true, + distinctId: 'test-host', + projectName: 'my-project', + tags: {}, + maxAttributeLength: 12000, } const privacyConfig: PluginConfig = { - ...defaultConfig, - privacyMode: true, + ...defaultConfig, + privacyMode: true, } const configWithTags: PluginConfig = { - ...defaultConfig, - tags: { team: "platform", env: "staging" }, + ...defaultConfig, + tags: { team: 'platform', env: 'staging' }, } function makeStepFinish(overrides?: Partial): StepFinishPart { - return { - id: "part-1", - sessionID: "session-1", - messageID: "msg-1", - type: "step-finish", - reason: "stop", - cost: 0.003, - tokens: { - input: 100, - output: 50, - reasoning: 10, - cache: { read: 5, write: 3 }, - }, - ...overrides, - } + return { + id: 'part-1', + sessionID: 'session-1', + messageID: 'msg-1', + type: 'step-finish', + reason: 'stop', + cost: 0.003, + tokens: { + input: 100, + output: 50, + reasoning: 10, + cache: { read: 5, write: 3 }, + }, + ...overrides, + } } function makeTrace(overrides?: Partial): TraceState { - return { - traceId: "trace-123", - sessionId: "session-1", - startTime: Date.now() - 5000, - totalInputTokens: 500, - totalOutputTokens: 200, - totalCost: 0.01, - hadError: false, - agentName: "my-project", - currentGenerationSpanId: "gen-span-1", - userPrompt: "Hello", - lastAssistantText: "Hi there!", - stepInputMessages: [{ role: "user", content: "Hello" }], - stepAssistantText: "Hi there!", - messageIds: new Set(), - ...overrides, - } + return { + traceId: 'trace-123', + sessionId: 'session-1', + startTime: Date.now() - 5000, + totalInputTokens: 500, + totalOutputTokens: 200, + totalCost: 0.01, + hadError: false, + agentName: 'my-project', + currentGenerationSpanId: 'gen-span-1', + userPrompt: 'Hello', + lastAssistantText: 'Hi there!', + stepInputMessages: [{ role: 'user', content: 'Hello' }], + stepAssistantText: 'Hi there!', + messageIds: new Set(), + ...overrides, + } } function makeAssistantInfo(overrides?: Partial): AssistantInfo { - return { - messageID: "msg-1", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - ...overrides, - } + return { + messageID: 'msg-1', + modelID: 'claude-sonnet-4-20250514', + providerID: 'anthropic', + ...overrides, + } } -describe("mapStopReason", () => { - it("maps known stop reasons", () => { - expect(mapStopReason("stop")).toBe("stop") - expect(mapStopReason("length")).toBe("length") - expect(mapStopReason("tool-calls")).toBe("tool_calls") - expect(mapStopReason("error")).toBe("error") - }) - - it("returns null for undefined", () => { - expect(mapStopReason(undefined)).toBeNull() - }) - - it("passes through unknown reasons", () => { - expect(mapStopReason("custom_reason")).toBe("custom_reason") - }) +describe('mapStopReason', () => { + it('maps known stop reasons', () => { + expect(mapStopReason('stop')).toBe('stop') + expect(mapStopReason('length')).toBe('length') + expect(mapStopReason('tool-calls')).toBe('tool_calls') + expect(mapStopReason('error')).toBe('error') + }) + + it('returns null for undefined', () => { + expect(mapStopReason(undefined)).toBeNull() + }) + + it('passes through unknown reasons', () => { + expect(mapStopReason('custom_reason')).toBe('custom_reason') + }) }) -describe("buildAiGeneration", () => { - it("builds generation event with all fields", () => { - const part = makeStepFinish() - const trace = makeTrace() - const assistant = makeAssistantInfo() - - const result = buildAiGeneration(part, assistant, trace, defaultConfig) - - expect(result.event).toBe("$ai_generation") - expect(result.distinctId).toBe("test-host") - expect(result.properties.$ai_model).toBe("claude-sonnet-4-20250514") - expect(result.properties.$ai_provider).toBe("anthropic") - expect(result.properties.$ai_input_tokens).toBe(100) - expect(result.properties.$ai_output_tokens).toBe(50) - expect(result.properties.$ai_reasoning_tokens).toBe(10) - expect(result.properties.$ai_cache_read_input_tokens).toBe(5) - expect(result.properties.$ai_cache_creation_input_tokens).toBe(3) - expect(result.properties.$ai_total_cost_usd).toBe(0.003) - expect(result.properties.$ai_stop_reason).toBe("stop") - expect(result.properties.$ai_is_error).toBe(false) - expect(result.properties.$ai_trace_id).toBe("trace-123") - expect(result.properties.$ai_span_id).toBe("gen-span-1") - expect(result.properties.$ai_session_id).toBe("session-1") - expect(result.properties.$ai_lib).toBe("opencode-posthog") - expect(result.properties.$ai_framework).toBe("opencode") - expect(result.properties.$ai_project_name).toBe("my-project") - expect(result.properties.$ai_agent_name).toBe("my-project") - }) - - it("uses pre-allocated span ID from trace", () => { - const trace = makeTrace({ currentGenerationSpanId: "pre-allocated-id" }) - const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) - expect(result.properties.$ai_span_id).toBe("pre-allocated-id") - }) - - it("falls back to random UUID when no pre-allocated span ID", () => { - const trace = makeTrace({ currentGenerationSpanId: undefined }) - const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) - expect(result.properties.$ai_span_id).toBeDefined() - expect(typeof result.properties.$ai_span_id).toBe("string") - }) - - it("includes input and output content from step messages", () => { - const trace = makeTrace({ - userPrompt: "What is 2+2?", - stepInputMessages: [{ role: "user", content: "What is 2+2?" }], - stepAssistantText: "4", - lastAssistantText: "4", +describe('buildAiGeneration', () => { + it('builds generation event with all fields', () => { + const part = makeStepFinish() + const trace = makeTrace() + const assistant = makeAssistantInfo() + + const result = buildAiGeneration(part, assistant, trace, defaultConfig) + + expect(result.event).toBe('$ai_generation') + expect(result.distinctId).toBe('test-host') + expect(result.properties.$ai_model).toBe('claude-sonnet-4-20250514') + expect(result.properties.$ai_provider).toBe('anthropic') + expect(result.properties.$ai_input_tokens).toBe(100) + expect(result.properties.$ai_output_tokens).toBe(50) + expect(result.properties.$ai_reasoning_tokens).toBe(10) + expect(result.properties.$ai_cache_read_input_tokens).toBe(5) + expect(result.properties.$ai_cache_creation_input_tokens).toBe(3) + expect(result.properties.$ai_total_cost_usd).toBe(0.003) + expect(result.properties.$ai_stop_reason).toBe('stop') + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_trace_id).toBe('trace-123') + expect(result.properties.$ai_span_id).toBe('gen-span-1') + expect(result.properties.$ai_session_id).toBe('session-1') + expect(result.properties.$ai_lib).toBe('opencode-posthog') + expect(result.properties.$ai_framework).toBe('opencode') + expect(result.properties.$ai_project_name).toBe('my-project') + expect(result.properties.$ai_agent_name).toBe('my-project') + }) + + it('uses pre-allocated span ID from trace', () => { + const trace = makeTrace({ currentGenerationSpanId: 'pre-allocated-id' }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_span_id).toBe('pre-allocated-id') + }) + + it('falls back to random UUID when no pre-allocated span ID', () => { + const trace = makeTrace({ currentGenerationSpanId: undefined }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_span_id).toBeDefined() + expect(typeof result.properties.$ai_span_id).toBe('string') + }) + + it('includes input and output content from step messages', () => { + const trace = makeTrace({ + userPrompt: 'What is 2+2?', + stepInputMessages: [{ role: 'user', content: 'What is 2+2?' }], + stepAssistantText: '4', + lastAssistantText: '4', + }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_input).toEqual([{ role: 'user', content: 'What is 2+2?' }]) + expect(result.properties.$ai_output_choices).toEqual([{ role: 'assistant', content: '4' }]) }) - const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) - expect(result.properties.$ai_input).toEqual([{ role: "user", content: "What is 2+2?" }]) - expect(result.properties.$ai_output_choices).toEqual([{ role: "assistant", content: "4" }]) - }) - - it("includes tool results in input for multi-step generations", () => { - const trace = makeTrace({ - stepInputMessages: [ - { role: "user", content: "Read the file" }, - { role: "tool", content: "[read] file contents here" }, - ], - stepAssistantText: "I read the file.", + + it('includes tool results in input for multi-step generations', () => { + const trace = makeTrace({ + stepInputMessages: [ + { role: 'user', content: 'Read the file' }, + { role: 'tool', content: '[read] file contents here' }, + ], + stepAssistantText: 'I read the file.', + }) + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) + expect(result.properties.$ai_input).toEqual([ + { role: 'user', content: 'Read the file' }, + { role: 'tool', content: '[read] file contents here' }, + ]) + expect(result.properties.$ai_output_choices).toEqual([{ role: 'assistant', content: 'I read the file.' }]) + }) + + it('redacts content in privacy mode', () => { + const trace = makeTrace() + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, privacyConfig) + expect(result.properties.$ai_input).toBeNull() + expect(result.properties.$ai_output_choices).toBeNull() }) - const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, defaultConfig) - expect(result.properties.$ai_input).toEqual([ - { role: "user", content: "Read the file" }, - { role: "tool", content: "[read] file contents here" }, - ]) - expect(result.properties.$ai_output_choices).toEqual([ - { role: "assistant", content: "I read the file." }, - ]) - }) - - it("redacts content in privacy mode", () => { - const trace = makeTrace() - const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), trace, privacyConfig) - expect(result.properties.$ai_input).toBeNull() - expect(result.properties.$ai_output_choices).toBeNull() - }) - - it("marks error generations", () => { - const assistant = makeAssistantInfo({ - error: { name: "UnknownError", data: { message: "Rate limited" } }, + + it('marks error generations', () => { + const assistant = makeAssistantInfo({ + error: { name: 'UnknownError', data: { message: 'Rate limited' } }, + }) + const result = buildAiGeneration(makeStepFinish(), assistant, makeTrace(), defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toContain('Rate limited') + }) + + it('falls back to unknown model when no assistant info', () => { + const result = buildAiGeneration(makeStepFinish(), undefined, makeTrace(), defaultConfig) + expect(result.properties.$ai_model).toBe('unknown') + expect(result.properties.$ai_provider).toBe('unknown') + }) + + it('includes custom tags', () => { + const result = buildAiGeneration(makeStepFinish(), makeAssistantInfo(), makeTrace(), configWithTags) + expect(result.properties.team).toBe('platform') + expect(result.properties.env).toBe('staging') }) - const result = buildAiGeneration(makeStepFinish(), assistant, makeTrace(), defaultConfig) - expect(result.properties.$ai_is_error).toBe(true) - expect(result.properties.$ai_error).toContain("Rate limited") - }) - - it("falls back to unknown model when no assistant info", () => { - const result = buildAiGeneration(makeStepFinish(), undefined, makeTrace(), defaultConfig) - expect(result.properties.$ai_model).toBe("unknown") - expect(result.properties.$ai_provider).toBe("unknown") - }) - - it("includes custom tags", () => { - const result = buildAiGeneration( - makeStepFinish(), - makeAssistantInfo(), - makeTrace(), - configWithTags, - ) - expect(result.properties.team).toBe("platform") - expect(result.properties.env).toBe("staging") - }) }) -describe("buildAiSpan", () => { - const completedState: ToolStateCompleted = { - status: "completed", - input: { command: "ls -la" }, - output: "file1.txt\nfile2.txt", - title: "bash", - metadata: {}, - time: { start: 1000, end: 1250 }, - } - - const errorState: ToolStateError = { - status: "error", - input: { command: "bad-cmd" }, - error: "command not found", - time: { start: 1000, end: 1050 }, - } - - it("builds span event for completed tool", () => { - const trace = makeTrace() - const result = buildAiSpan("bash", completedState, trace, defaultConfig) - - expect(result.event).toBe("$ai_span") - expect(result.distinctId).toBe("test-host") - expect(result.properties.$ai_trace_id).toBe("trace-123") - expect(result.properties.$ai_parent_id).toBe("gen-span-1") - expect(result.properties.$ai_span_name).toBe("bash") - expect(result.properties.$ai_latency).toBe(0.25) - expect(result.properties.$ai_is_error).toBe(false) - expect(result.properties.$ai_error).toBeNull() - expect(result.properties.$ai_input_state).toBe('{"command":"ls -la"}') - expect(result.properties.$ai_output_state).toBe("file1.txt\nfile2.txt") - expect(result.properties.$ai_lib).toBe("opencode-posthog") - expect(result.properties.$ai_framework).toBe("opencode") - }) - - it("uses currentGenerationSpanId as parent", () => { - const trace = makeTrace({ currentGenerationSpanId: "parent-gen-42" }) - const result = buildAiSpan("read", completedState, trace, defaultConfig) - expect(result.properties.$ai_parent_id).toBe("parent-gen-42") - }) - - it("sets parent to null when no generation span ID", () => { - const trace = makeTrace({ currentGenerationSpanId: undefined }) - const result = buildAiSpan("read", completedState, trace, defaultConfig) - expect(result.properties.$ai_parent_id).toBeNull() - }) - - it("redacts tool input/output in privacy mode", () => { - const result = buildAiSpan("read", completedState, makeTrace(), privacyConfig) - expect(result.properties.$ai_input_state).toBeNull() - expect(result.properties.$ai_output_state).toBeNull() - }) - - it("redacts sensitive keys in tool input", () => { - const stateWithSecrets: ToolStateCompleted = { - ...completedState, - input: { - command: "curl", - api_key: "sk-secret-123", - headers: { authorization: "Bearer tok" }, - }, +describe('buildAiSpan', () => { + const completedState: ToolStateCompleted = { + status: 'completed', + input: { command: 'ls -la' }, + output: 'file1.txt\nfile2.txt', + title: 'bash', + metadata: {}, + time: { start: 1000, end: 1250 }, } - const result = buildAiSpan("bash", stateWithSecrets, makeTrace(), defaultConfig) - const inputState = result.properties.$ai_input_state as string - expect(inputState).toContain("[REDACTED]") - expect(inputState).not.toContain("sk-secret-123") - expect(inputState).not.toContain("Bearer tok") - expect(inputState).toContain("curl") - }) - - it("captures error info", () => { - const result = buildAiSpan("bash", errorState, makeTrace(), defaultConfig) - expect(result.properties.$ai_is_error).toBe(true) - expect(result.properties.$ai_error).toBe("command not found") - expect(result.properties.$ai_latency).toBe(0.05) - }) - - it("includes custom tags", () => { - const result = buildAiSpan("read", completedState, makeTrace(), configWithTags) - expect(result.properties.team).toBe("platform") - expect(result.properties.env).toBe("staging") - }) + + const errorState: ToolStateError = { + status: 'error', + input: { command: 'bad-cmd' }, + error: 'command not found', + time: { start: 1000, end: 1050 }, + } + + it('builds span event for completed tool', () => { + const trace = makeTrace() + const result = buildAiSpan('bash', completedState, trace, defaultConfig) + + expect(result.event).toBe('$ai_span') + expect(result.distinctId).toBe('test-host') + expect(result.properties.$ai_trace_id).toBe('trace-123') + expect(result.properties.$ai_parent_id).toBe('gen-span-1') + expect(result.properties.$ai_span_name).toBe('bash') + expect(result.properties.$ai_latency).toBe(0.25) + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_error).toBeNull() + expect(result.properties.$ai_input_state).toBe('{"command":"ls -la"}') + expect(result.properties.$ai_output_state).toBe('file1.txt\nfile2.txt') + expect(result.properties.$ai_lib).toBe('opencode-posthog') + expect(result.properties.$ai_framework).toBe('opencode') + }) + + it('uses currentGenerationSpanId as parent', () => { + const trace = makeTrace({ currentGenerationSpanId: 'parent-gen-42' }) + const result = buildAiSpan('read', completedState, trace, defaultConfig) + expect(result.properties.$ai_parent_id).toBe('parent-gen-42') + }) + + it('sets parent to null when no generation span ID', () => { + const trace = makeTrace({ currentGenerationSpanId: undefined }) + const result = buildAiSpan('read', completedState, trace, defaultConfig) + expect(result.properties.$ai_parent_id).toBeNull() + }) + + it('redacts tool input/output in privacy mode', () => { + const result = buildAiSpan('read', completedState, makeTrace(), privacyConfig) + expect(result.properties.$ai_input_state).toBeNull() + expect(result.properties.$ai_output_state).toBeNull() + }) + + it('redacts sensitive keys in tool input', () => { + const stateWithSecrets: ToolStateCompleted = { + ...completedState, + input: { + command: 'curl', + api_key: 'sk-secret-123', + headers: { authorization: 'Bearer tok' }, + }, + } + const result = buildAiSpan('bash', stateWithSecrets, makeTrace(), defaultConfig) + const inputState = result.properties.$ai_input_state as string + expect(inputState).toContain('[REDACTED]') + expect(inputState).not.toContain('sk-secret-123') + expect(inputState).not.toContain('Bearer tok') + expect(inputState).toContain('curl') + }) + + it('captures error info', () => { + const result = buildAiSpan('bash', errorState, makeTrace(), defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toBe('command not found') + expect(result.properties.$ai_latency).toBe(0.05) + }) + + it('includes custom tags', () => { + const result = buildAiSpan('read', completedState, makeTrace(), configWithTags) + expect(result.properties.team).toBe('platform') + expect(result.properties.env).toBe('staging') + }) }) -describe("buildAiTrace", () => { - it("builds trace event with accumulated totals", () => { - const trace = makeTrace() - const result = buildAiTrace(trace, defaultConfig) - - expect(result.event).toBe("$ai_trace") - expect(result.distinctId).toBe("test-host") - expect(result.properties.$ai_trace_id).toBe("trace-123") - expect(result.properties.$ai_session_id).toBe("session-1") - expect(result.properties.$ai_latency).toBeGreaterThan(0) - expect(result.properties.$ai_total_input_tokens).toBe(500) - expect(result.properties.$ai_total_output_tokens).toBe(200) - expect(result.properties.$ai_is_error).toBe(false) - expect(result.properties.$ai_span_name).toBe("my-project") - expect(result.properties.$ai_lib).toBe("opencode-posthog") - expect(result.properties.$ai_framework).toBe("opencode") - }) - - it("includes user prompt and assistant text", () => { - const trace = makeTrace({ - userPrompt: "Explain X", - lastAssistantText: "X is...", +describe('buildAiTrace', () => { + it('builds trace event with accumulated totals', () => { + const trace = makeTrace() + const result = buildAiTrace(trace, defaultConfig) + + expect(result.event).toBe('$ai_trace') + expect(result.distinctId).toBe('test-host') + expect(result.properties.$ai_trace_id).toBe('trace-123') + expect(result.properties.$ai_session_id).toBe('session-1') + expect(result.properties.$ai_latency).toBeGreaterThan(0) + expect(result.properties.$ai_total_input_tokens).toBe(500) + expect(result.properties.$ai_total_output_tokens).toBe(200) + expect(result.properties.$ai_is_error).toBe(false) + expect(result.properties.$ai_span_name).toBe('my-project') + expect(result.properties.$ai_lib).toBe('opencode-posthog') + expect(result.properties.$ai_framework).toBe('opencode') }) - const result = buildAiTrace(trace, defaultConfig) - expect(result.properties.$ai_input_state).toBe("Explain X") - expect(result.properties.$ai_output_state).toBe("X is...") - }) - - it("redacts content in privacy mode", () => { - const trace = makeTrace({ - userPrompt: "secret prompt", - lastAssistantText: "secret response", + + it('includes user prompt and assistant text', () => { + const trace = makeTrace({ + userPrompt: 'Explain X', + lastAssistantText: 'X is...', + }) + const result = buildAiTrace(trace, defaultConfig) + expect(result.properties.$ai_input_state).toBe('Explain X') + expect(result.properties.$ai_output_state).toBe('X is...') }) - const result = buildAiTrace(trace, privacyConfig) - expect(result.properties.$ai_input_state).toBeNull() - expect(result.properties.$ai_output_state).toBeNull() - // Metrics still flow - expect(result.properties.$ai_total_input_tokens).toBe(500) - expect(result.properties.$ai_total_output_tokens).toBe(200) - }) - - it("captures error traces", () => { - const trace = makeTrace({ - hadError: true, - lastError: "Context overflow", + + it('redacts content in privacy mode', () => { + const trace = makeTrace({ + userPrompt: 'secret prompt', + lastAssistantText: 'secret response', + }) + const result = buildAiTrace(trace, privacyConfig) + expect(result.properties.$ai_input_state).toBeNull() + expect(result.properties.$ai_output_state).toBeNull() + // Metrics still flow + expect(result.properties.$ai_total_input_tokens).toBe(500) + expect(result.properties.$ai_total_output_tokens).toBe(200) + }) + + it('captures error traces', () => { + const trace = makeTrace({ + hadError: true, + lastError: 'Context overflow', + }) + const result = buildAiTrace(trace, defaultConfig) + expect(result.properties.$ai_is_error).toBe(true) + expect(result.properties.$ai_error).toBe('Context overflow') + }) + + it('includes custom tags', () => { + const result = buildAiTrace(makeTrace(), configWithTags) + expect(result.properties.team).toBe('platform') + expect(result.properties.env).toBe('staging') }) - const result = buildAiTrace(trace, defaultConfig) - expect(result.properties.$ai_is_error).toBe(true) - expect(result.properties.$ai_error).toBe("Context overflow") - }) - - it("includes custom tags", () => { - const result = buildAiTrace(makeTrace(), configWithTags) - expect(result.properties.team).toBe("platform") - expect(result.properties.env).toBe("staging") - }) }) diff --git a/src/events.ts b/src/events.ts index 41f1fa5..5fb2495 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,175 +1,165 @@ -import { randomUUID } from "node:crypto" -import type { StepFinishPart, ToolStateCompleted, ToolStateError } from "@opencode-ai/sdk" -import type { AssistantInfo, PluginConfig, TraceState } from "./types.js" -import { redactForPrivacy, serializeAttribute, serializeError } from "./utils.js" -import { VERSION } from "./version.js" +import { randomUUID } from 'node:crypto' +import type { StepFinishPart, ToolStateCompleted, ToolStateError } from '@opencode-ai/sdk' +import type { AssistantInfo, PluginConfig, TraceState } from './types.js' +import { redactForPrivacy, serializeAttribute, serializeError } from './utils.js' +import { VERSION } from './version.js' export interface CaptureEvent { - event: string - distinctId: string - properties: Record + event: string + distinctId: string + properties: Record } const STOP_REASON_MAP: Record = { - stop: "stop", - length: "length", - "tool-calls": "tool_calls", - error: "error", + stop: 'stop', + length: 'length', + 'tool-calls': 'tool_calls', + error: 'error', } export function mapStopReason(reason: string | undefined): string | null { - if (!reason) return null - return STOP_REASON_MAP[reason] ?? reason + if (!reason) return null + return STOP_REASON_MAP[reason] ?? reason } export function buildAiGeneration( - part: StepFinishPart, - assistantInfo: AssistantInfo | undefined, - trace: TraceState, - config: PluginConfig, + part: StepFinishPart, + assistantInfo: AssistantInfo | undefined, + trace: TraceState, + config: PluginConfig ): CaptureEvent { - // Use the span ID allocated at step-start so tool spans emitted - // during this step already reference the correct parent. - const spanId = trace.currentGenerationSpanId ?? randomUUID() - - // Use accumulated step input messages (includes user prompt + tool results) - // for accurate per-roundtrip context. - const inputMessages = redactForPrivacy( - trace.stepInputMessages.length > 0 ? trace.stepInputMessages : null, - config.privacyMode, - ) - - const outputChoices = redactForPrivacy( - trace.stepAssistantText - ? [{ role: "assistant", content: trace.stepAssistantText }] - : null, - config.privacyMode, - ) - - return { - event: "$ai_generation", - distinctId: config.distinctId, - properties: { - $ai_trace_id: trace.traceId, - $ai_session_id: trace.sessionId, - $ai_span_id: spanId, - $ai_model: assistantInfo?.modelID ?? "unknown", - $ai_provider: assistantInfo?.providerID ?? "unknown", - - $ai_input_tokens: part.tokens.input, - $ai_output_tokens: part.tokens.output, - $ai_reasoning_tokens: part.tokens.reasoning, - $ai_cache_read_input_tokens: part.tokens.cache.read, - $ai_cache_creation_input_tokens: part.tokens.cache.write, - - $ai_total_cost_usd: part.cost, - $ai_stop_reason: mapStopReason(part.reason), - - $ai_input: inputMessages, - $ai_output_choices: outputChoices, - - $ai_is_error: assistantInfo?.error ? true : false, - $ai_error: serializeError(assistantInfo?.error, config.maxAttributeLength), - - $ai_lib: "opencode-posthog", - $ai_lib_version: VERSION, - $ai_framework: "opencode", - $ai_project_name: config.projectName, - $ai_agent_name: trace.agentName ?? config.projectName, - ...config.tags, - }, - } + // Use the span ID allocated at step-start so tool spans emitted + // during this step already reference the correct parent. + const spanId = trace.currentGenerationSpanId ?? randomUUID() + + // Use accumulated step input messages (includes user prompt + tool results) + // for accurate per-roundtrip context. + const inputMessages = redactForPrivacy( + trace.stepInputMessages.length > 0 ? trace.stepInputMessages : null, + config.privacyMode + ) + + const outputChoices = redactForPrivacy( + trace.stepAssistantText ? [{ role: 'assistant', content: trace.stepAssistantText }] : null, + config.privacyMode + ) + + return { + event: '$ai_generation', + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_span_id: spanId, + $ai_model: assistantInfo?.modelID ?? 'unknown', + $ai_provider: assistantInfo?.providerID ?? 'unknown', + + $ai_input_tokens: part.tokens.input, + $ai_output_tokens: part.tokens.output, + $ai_reasoning_tokens: part.tokens.reasoning, + $ai_cache_read_input_tokens: part.tokens.cache.read, + $ai_cache_creation_input_tokens: part.tokens.cache.write, + + $ai_total_cost_usd: part.cost, + $ai_stop_reason: mapStopReason(part.reason), + + $ai_input: inputMessages, + $ai_output_choices: outputChoices, + + $ai_is_error: !!assistantInfo?.error, + $ai_error: serializeError(assistantInfo?.error, config.maxAttributeLength), + + $ai_lib: 'opencode-posthog', + $ai_lib_version: VERSION, + $ai_framework: 'opencode', + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, + } } export function buildAiSpan( - toolName: string, - toolState: ToolStateCompleted | ToolStateError, - trace: TraceState, - config: PluginConfig, + toolName: string, + toolState: ToolStateCompleted | ToolStateError, + trace: TraceState, + config: PluginConfig ): CaptureEvent { - const spanId = randomUUID() - const latency = (toolState.time.end - toolState.time.start) / 1000 - const isError = toolState.status === "error" - - const inputState = redactForPrivacy( - serializeAttribute(toolState.input, config.maxAttributeLength), - config.privacyMode, - ) - - let outputState: string | null = null - if (!config.privacyMode) { - if (toolState.status === "completed") { - outputState = serializeAttribute(toolState.output, config.maxAttributeLength) - } else { - outputState = serializeAttribute(toolState.error, config.maxAttributeLength) + const spanId = randomUUID() + const latency = (toolState.time.end - toolState.time.start) / 1000 + const isError = toolState.status === 'error' + + const inputState = redactForPrivacy( + serializeAttribute(toolState.input, config.maxAttributeLength), + config.privacyMode + ) + + let outputState: string | null = null + if (!config.privacyMode) { + if (toolState.status === 'completed') { + outputState = serializeAttribute(toolState.output, config.maxAttributeLength) + } else { + outputState = serializeAttribute(toolState.error, config.maxAttributeLength) + } + } + + return { + event: '$ai_span', + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_span_id: spanId, + $ai_parent_id: trace.currentGenerationSpanId ?? null, + $ai_span_name: toolName, + + $ai_latency: latency, + + $ai_input_state: inputState, + $ai_output_state: outputState, + + $ai_is_error: isError, + $ai_error: isError + ? serializeAttribute((toolState as ToolStateError).error, config.maxAttributeLength) + : null, + + $ai_lib: 'opencode-posthog', + $ai_lib_version: VERSION, + $ai_framework: 'opencode', + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, } - } - - return { - event: "$ai_span", - distinctId: config.distinctId, - properties: { - $ai_trace_id: trace.traceId, - $ai_session_id: trace.sessionId, - $ai_span_id: spanId, - $ai_parent_id: trace.currentGenerationSpanId ?? null, - $ai_span_name: toolName, - - $ai_latency: latency, - - $ai_input_state: inputState, - $ai_output_state: outputState, - - $ai_is_error: isError, - $ai_error: isError - ? serializeAttribute((toolState as ToolStateError).error, config.maxAttributeLength) - : null, - - $ai_lib: "opencode-posthog", - $ai_lib_version: VERSION, - $ai_framework: "opencode", - $ai_project_name: config.projectName, - $ai_agent_name: trace.agentName ?? config.projectName, - ...config.tags, - }, - } } -export function buildAiTrace( - trace: TraceState, - config: PluginConfig, -): CaptureEvent { - const latency = (Date.now() - trace.startTime) / 1000 - - return { - event: "$ai_trace", - distinctId: config.distinctId, - properties: { - $ai_trace_id: trace.traceId, - $ai_session_id: trace.sessionId, - $ai_latency: latency, - $ai_span_name: config.projectName, - - $ai_input_state: redactForPrivacy(trace.userPrompt ?? null, config.privacyMode), - $ai_output_state: redactForPrivacy( - trace.lastAssistantText ?? null, - config.privacyMode, - ), - - $ai_total_input_tokens: trace.totalInputTokens, - $ai_total_output_tokens: trace.totalOutputTokens, - - $ai_is_error: trace.hadError, - $ai_error: trace.lastError - ? serializeAttribute(trace.lastError, config.maxAttributeLength) - : null, - - $ai_lib: "opencode-posthog", - $ai_lib_version: VERSION, - $ai_framework: "opencode", - $ai_project_name: config.projectName, - $ai_agent_name: trace.agentName ?? config.projectName, - ...config.tags, - }, - } +export function buildAiTrace(trace: TraceState, config: PluginConfig): CaptureEvent { + const latency = (Date.now() - trace.startTime) / 1000 + + return { + event: '$ai_trace', + distinctId: config.distinctId, + properties: { + $ai_trace_id: trace.traceId, + $ai_session_id: trace.sessionId, + $ai_latency: latency, + $ai_span_name: config.projectName, + + $ai_input_state: redactForPrivacy(trace.userPrompt ?? null, config.privacyMode), + $ai_output_state: redactForPrivacy(trace.lastAssistantText ?? null, config.privacyMode), + + $ai_total_input_tokens: trace.totalInputTokens, + $ai_total_output_tokens: trace.totalOutputTokens, + + $ai_is_error: trace.hadError, + $ai_error: trace.lastError ? serializeAttribute(trace.lastError, config.maxAttributeLength) : null, + + $ai_lib: 'opencode-posthog', + $ai_lib_version: VERSION, + $ai_framework: 'opencode', + $ai_project_name: config.projectName, + $ai_agent_name: trace.agentName ?? config.projectName, + ...config.tags, + }, + } } diff --git a/src/index.ts b/src/index.ts index e8cf963..6a273ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,263 +1,264 @@ -import type { Plugin } from "@opencode-ai/plugin" +import type { Plugin } from '@opencode-ai/plugin' import type { - Event, - AssistantMessage, - StepStartPart, - StepFinishPart, - ToolPart, - TextPart, - ToolStateCompleted, - ToolStateError, -} from "@opencode-ai/sdk" -import { PostHog } from "posthog-node" -import { randomUUID } from "node:crypto" -import { loadConfig, serializeAttribute } from "./utils.js" -import { buildAiGeneration, buildAiSpan, buildAiTrace } from "./events.js" -import type { AssistantInfo, TraceState } from "./types.js" -import type { CaptureEvent } from "./events.js" + Event, + AssistantMessage, + StepStartPart, + StepFinishPart, + ToolPart, + TextPart, + ToolStateCompleted, + ToolStateError, +} from '@opencode-ai/sdk' +import { PostHog } from 'posthog-node' +import { randomUUID } from 'node:crypto' +import { loadConfig, serializeAttribute } from './utils.js' +import { buildAiGeneration, buildAiSpan, buildAiTrace } from './events.js' +import type { AssistantInfo, TraceState } from './types.js' +import type { CaptureEvent } from './events.js' export const PostHogPlugin: Plugin = async () => { - const config = loadConfig() - - if (!config.enabled || !config.apiKey) return {} - - const client = new PostHog(config.apiKey, { - host: config.host, - flushAt: 20, - flushInterval: 10_000, - }) - - function safeCapture(event: CaptureEvent) { - try { - client.capture({ - distinctId: event.distinctId, - event: event.event, - properties: event.properties, - }) - } catch { - // never crash the host + const config = loadConfig() + + if (!config.enabled || !config.apiKey) return {} + + const client = new PostHog(config.apiKey, { + host: config.host, + flushAt: 20, + flushInterval: 10_000, + }) + + function safeCapture(event: CaptureEvent) { + try { + client.capture({ + distinctId: event.distinctId, + event: event.event, + properties: event.properties, + }) + } catch { + // never crash the host + } } - } - - // State: sessionID -> trace state - const traces = new Map() - // State: messageID -> role for correlating parts to messages - const messageRoles = new Map() - // State: messageID -> assistant info - const assistantMessages = new Map() - - function getOrCreateTrace(sessionId: string): TraceState { - let trace = traces.get(sessionId) - if (!trace) { - trace = { - traceId: randomUUID(), - sessionId, - startTime: Date.now(), - totalInputTokens: 0, - totalOutputTokens: 0, - totalCost: 0, - hadError: false, - stepInputMessages: [], - messageIds: new Set(), - } - traces.set(sessionId, trace) + + // State: sessionID -> trace state + const traces = new Map() + // State: messageID -> role for correlating parts to messages + const messageRoles = new Map() + // State: messageID -> assistant info + const assistantMessages = new Map() + + function getOrCreateTrace(sessionId: string): TraceState { + let trace = traces.get(sessionId) + if (!trace) { + trace = { + traceId: randomUUID(), + sessionId, + startTime: Date.now(), + totalInputTokens: 0, + totalOutputTokens: 0, + totalCost: 0, + hadError: false, + stepInputMessages: [], + messageIds: new Set(), + } + traces.set(sessionId, trace) + } + return trace } - return trace - } - - function handleMessageUpdated(event: Event) { - if (event.type !== "message.updated") return - const msg = event.properties.info - - if (msg.role === "user") { - // New user message → new trace - const trace: TraceState = { - traceId: randomUUID(), - sessionId: msg.sessionID, - startTime: msg.time.created, - totalInputTokens: 0, - totalOutputTokens: 0, - totalCost: 0, - hadError: false, - agentName: msg.agent, - stepInputMessages: [], - messageIds: new Set([msg.id]), - } - traces.set(msg.sessionID, trace) - messageRoles.set(msg.id, "user") - } else if (msg.role === "assistant") { - const assistant = msg as AssistantMessage - messageRoles.set(assistant.id, "assistant") - - const info: AssistantInfo = { - messageID: assistant.id, - modelID: assistant.modelID, - providerID: assistant.providerID, - error: assistant.error, - } - assistantMessages.set(assistant.id, info) - - // Update trace with current assistant info - const trace = getOrCreateTrace(assistant.sessionID) - trace.messageIds.add(assistant.id) - trace.currentAssistantMsg = info - if (assistant.error) { - trace.hadError = true - trace.lastError = serializeAttribute(assistant.error, config.maxAttributeLength) ?? assistant.error.name - } + + function handleMessageUpdated(event: Event) { + if (event.type !== 'message.updated') return + const msg = event.properties.info + + if (msg.role === 'user') { + // New user message → new trace + const trace: TraceState = { + traceId: randomUUID(), + sessionId: msg.sessionID, + startTime: msg.time.created, + totalInputTokens: 0, + totalOutputTokens: 0, + totalCost: 0, + hadError: false, + agentName: msg.agent, + stepInputMessages: [], + messageIds: new Set([msg.id]), + } + traces.set(msg.sessionID, trace) + messageRoles.set(msg.id, 'user') + } else if (msg.role === 'assistant') { + const assistant = msg as AssistantMessage + messageRoles.set(assistant.id, 'assistant') + + const info: AssistantInfo = { + messageID: assistant.id, + modelID: assistant.modelID, + providerID: assistant.providerID, + error: assistant.error, + } + assistantMessages.set(assistant.id, info) + + // Update trace with current assistant info + const trace = getOrCreateTrace(assistant.sessionID) + trace.messageIds.add(assistant.id) + trace.currentAssistantMsg = info + if (assistant.error) { + trace.hadError = true + trace.lastError = serializeAttribute(assistant.error, config.maxAttributeLength) ?? assistant.error.name + } + } } - } - - function handlePartUpdated(event: Event) { - if (event.type !== "message.part.updated") return - const part = event.properties.part - - switch (part.type) { - case "text": - handleTextPart(part) - break - case "step-start": - handleStepStart(part) - break - case "step-finish": - handleStepFinish(part) - break - case "tool": - handleToolPart(part) - break + + function handlePartUpdated(event: Event) { + if (event.type !== 'message.part.updated') return + const part = event.properties.part + + switch (part.type) { + case 'text': + handleTextPart(part) + break + case 'step-start': + handleStepStart(part) + break + case 'step-finish': + handleStepFinish(part) + break + case 'tool': + handleToolPart(part) + break + } } - } - function handleTextPart(part: TextPart) { - const role = messageRoles.get(part.messageID) - if (!role) return + function handleTextPart(part: TextPart) { + const role = messageRoles.get(part.messageID) + if (!role) return - const trace = traces.get(part.sessionID) - if (!trace) return + const trace = traces.get(part.sessionID) + if (!trace) return - if (role === "user") { - trace.userPrompt = part.text - trace.stepInputMessages.push({ role: "user", content: part.text }) - } else if (role === "assistant") { - trace.lastAssistantText = part.text - trace.stepAssistantText = part.text + if (role === 'user') { + trace.userPrompt = part.text + trace.stepInputMessages.push({ role: 'user', content: part.text }) + } else if (role === 'assistant') { + trace.lastAssistantText = part.text + trace.stepAssistantText = part.text + } } - } - - function handleStepStart(part: StepStartPart) { - const trace = traces.get(part.sessionID) - if (!trace) return - // Allocate the generation span ID eagerly so that tool spans - // emitted during this step can reference it as their parent. - trace.currentGenerationSpanId = randomUUID() - // Reset per-step assistant text for the new generation - trace.stepAssistantText = undefined - } - - function handleStepFinish(part: StepFinishPart) { - const trace = traces.get(part.sessionID) - if (!trace) return - - const assistantInfo = trace.currentAssistantMsg - - // Accumulate tokens and cost - trace.totalInputTokens += part.tokens.input - trace.totalOutputTokens += part.tokens.output - trace.totalCost += part.cost - - const generation = buildAiGeneration(part, assistantInfo, trace, config) - safeCapture(generation) - } - - function handleToolPart(part: ToolPart) { - if (part.state.status !== "completed" && part.state.status !== "error") return - - const trace = traces.get(part.sessionID) - if (!trace) return - - const toolState = part.state as ToolStateCompleted | ToolStateError - const span = buildAiSpan(part.tool, toolState, trace, config) - safeCapture(span) - - // Feed tool result into step input so subsequent generations include - // the tool context the model actually saw. Redact and truncate to - // match the treatment applied to $ai_span fields. - if (toolState.status === "completed") { - const redacted = serializeAttribute(toolState.output, config.maxAttributeLength) ?? "" - trace.stepInputMessages.push({ - role: "tool", - content: `[${part.tool}] ${redacted}`, - }) - } else { - const redacted = serializeAttribute(toolState.error, config.maxAttributeLength) ?? "" - trace.stepInputMessages.push({ - role: "tool", - content: `[${part.tool}] ERROR: ${redacted}`, - }) - trace.hadError = true - trace.lastError = toolState.error + + function handleStepStart(part: StepStartPart) { + const trace = traces.get(part.sessionID) + if (!trace) return + // Allocate the generation span ID eagerly so that tool spans + // emitted during this step can reference it as their parent. + trace.currentGenerationSpanId = randomUUID() + // Reset per-step assistant text for the new generation + trace.stepAssistantText = undefined } - } - async function handleSessionIdle(event: Event) { - if (event.type !== "session.idle") return + function handleStepFinish(part: StepFinishPart) { + const trace = traces.get(part.sessionID) + if (!trace) return - const sessionId = event.properties.sessionID - const trace = traces.get(sessionId) - if (!trace) return + const assistantInfo = trace.currentAssistantMsg - const traceEvent = buildAiTrace(trace, config) - safeCapture(traceEvent) + // Accumulate tokens and cost + trace.totalInputTokens += part.tokens.input + trace.totalOutputTokens += part.tokens.output + trace.totalCost += part.cost - try { - await client.flush() - } catch { - // ignore flush errors + const generation = buildAiGeneration(part, assistantInfo, trace, config) + safeCapture(generation) } - // Clean up per-message state for this trace - for (const msgId of trace.messageIds) { - messageRoles.delete(msgId) - assistantMessages.delete(msgId) + function handleToolPart(part: ToolPart) { + if (part.state.status !== 'completed' && part.state.status !== 'error') return + + const trace = traces.get(part.sessionID) + if (!trace) return + + const toolState = part.state as ToolStateCompleted | ToolStateError + const span = buildAiSpan(part.tool, toolState, trace, config) + safeCapture(span) + + // Feed tool result into step input so subsequent generations include + // the tool context the model actually saw. Redact and truncate to + // match the treatment applied to $ai_span fields. + if (toolState.status === 'completed') { + const redacted = serializeAttribute(toolState.output, config.maxAttributeLength) ?? '' + trace.stepInputMessages.push({ + role: 'tool', + content: `[${part.tool}] ${redacted}`, + }) + } else { + const redacted = serializeAttribute(toolState.error, config.maxAttributeLength) ?? '' + trace.stepInputMessages.push({ + role: 'tool', + content: `[${part.tool}] ERROR: ${redacted}`, + }) + trace.hadError = true + trace.lastError = toolState.error + } } - traces.delete(sessionId) - } - function handleSessionError(event: Event) { - if (event.type !== "session.error") return + async function handleSessionIdle(event: Event) { + if (event.type !== 'session.idle') return - const sessionId = event.properties.sessionID - if (!sessionId) return + const sessionId = event.properties.sessionID + const trace = traces.get(sessionId) + if (!trace) return - const trace = traces.get(sessionId) - if (trace) { - trace.hadError = true - if (event.properties.error) { - trace.lastError = serializeAttribute(event.properties.error, config.maxAttributeLength) ?? "unknown error" - } + const traceEvent = buildAiTrace(trace, config) + safeCapture(traceEvent) + + try { + await client.flush() + } catch { + // ignore flush errors + } + + // Clean up per-message state for this trace + for (const msgId of trace.messageIds) { + messageRoles.delete(msgId) + assistantMessages.delete(msgId) + } + traces.delete(sessionId) } - } - - return { - event: async ({ event }) => { - try { - switch (event.type) { - case "message.updated": - handleMessageUpdated(event) - break - case "message.part.updated": - handlePartUpdated(event) - break - case "session.idle": - await handleSessionIdle(event) - break - case "session.error": - handleSessionError(event) - break + + function handleSessionError(event: Event) { + if (event.type !== 'session.error') return + + const sessionId = event.properties.sessionID + if (!sessionId) return + + const trace = traces.get(sessionId) + if (trace) { + trace.hadError = true + if (event.properties.error) { + trace.lastError = + serializeAttribute(event.properties.error, config.maxAttributeLength) ?? 'unknown error' + } } - } catch { - // never crash OpenCode - } - }, - } + } + + return { + event: async ({ event }) => { + try { + switch (event.type) { + case 'message.updated': + handleMessageUpdated(event) + break + case 'message.part.updated': + handlePartUpdated(event) + break + case 'session.idle': + await handleSessionIdle(event) + break + case 'session.error': + handleSessionError(event) + break + } + } catch { + // never crash OpenCode + } + }, + } } diff --git a/src/types.ts b/src/types.ts index da201fe..79d7bea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,44 +1,44 @@ export interface PluginConfig { - apiKey: string - host: string - privacyMode: boolean - enabled: boolean - distinctId: string - projectName: string - tags: Record - maxAttributeLength: number + apiKey: string + host: string + privacyMode: boolean + enabled: boolean + distinctId: string + projectName: string + tags: Record + maxAttributeLength: number } export interface InputMessage { - role: string - content: string + role: string + content: string } export interface TraceState { - traceId: string - sessionId: string - startTime: number - totalInputTokens: number - totalOutputTokens: number - totalCost: number - hadError: boolean - lastError?: string - userPrompt?: string - lastAssistantText?: string - /** Accumulated input context for the current step (user prompt + tool results). */ - stepInputMessages: InputMessage[] - /** Assistant text accumulated during the current step, reset on each step-start. */ - stepAssistantText?: string - currentAssistantMsg?: AssistantInfo - currentGenerationSpanId?: string - agentName?: string - /** Message IDs belonging to this trace, for cleanup. */ - messageIds: Set + traceId: string + sessionId: string + startTime: number + totalInputTokens: number + totalOutputTokens: number + totalCost: number + hadError: boolean + lastError?: string + userPrompt?: string + lastAssistantText?: string + /** Accumulated input context for the current step (user prompt + tool results). */ + stepInputMessages: InputMessage[] + /** Assistant text accumulated during the current step, reset on each step-start. */ + stepAssistantText?: string + currentAssistantMsg?: AssistantInfo + currentGenerationSpanId?: string + agentName?: string + /** Message IDs belonging to this trace, for cleanup. */ + messageIds: Set } export interface AssistantInfo { - messageID: string - modelID: string - providerID: string - error?: { name: string; data?: Record } + messageID: string + modelID: string + providerID: string + error?: { name: string; data?: Record } } diff --git a/src/utils.test.ts b/src/utils.test.ts index b273611..830ff30 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,140 +1,140 @@ -import { describe, it, expect } from "vitest" -import { redactForPrivacy, serializeAttribute, serializeError } from "./utils.js" - -describe("redactForPrivacy", () => { - it("returns value when privacy mode is off", () => { - expect(redactForPrivacy("hello", false)).toBe("hello") - expect(redactForPrivacy({ key: "val" }, false)).toEqual({ key: "val" }) - }) - - it("returns null when privacy mode is on", () => { - expect(redactForPrivacy("hello", true)).toBeNull() - expect(redactForPrivacy({ key: "val" }, true)).toBeNull() - }) +import { describe, it, expect } from 'vitest' +import { redactForPrivacy, serializeAttribute, serializeError } from './utils.js' + +describe('redactForPrivacy', () => { + it('returns value when privacy mode is off', () => { + expect(redactForPrivacy('hello', false)).toBe('hello') + expect(redactForPrivacy({ key: 'val' }, false)).toEqual({ key: 'val' }) + }) + + it('returns null when privacy mode is on', () => { + expect(redactForPrivacy('hello', true)).toBeNull() + expect(redactForPrivacy({ key: 'val' }, true)).toBeNull() + }) }) -describe("serializeAttribute", () => { - it("serializes simple values", () => { - expect(serializeAttribute({ a: 1 }, 1000)).toBe('{"a":1}') - expect(serializeAttribute("hello", 1000)).toBe("hello") - }) - - it("redacts sensitive keys in objects", () => { - const input = { - command: "curl", - api_key: "sk-secret-123", - apiKey: "another-secret", - token: "my-token", - password: "pass123", - authorization: "Bearer xyz", - normal_field: "visible", - } - const result = serializeAttribute(input, 10000) - expect(result).toContain("[REDACTED]") - expect(result).not.toContain("sk-secret-123") - expect(result).not.toContain("another-secret") - expect(result).not.toContain("my-token") - expect(result).not.toContain("pass123") - expect(result).not.toContain("Bearer xyz") - expect(result).toContain("curl") - expect(result).toContain("visible") - }) - - it("redacts nested sensitive keys", () => { - const input = { - headers: { Authorization: "Bearer secret" }, - config: { api_key: "hidden" }, - } - const result = serializeAttribute(input, 10000) - expect(result).not.toContain("secret") - expect(result).not.toContain("hidden") - }) - - it("redacts sensitive values embedded in strings", () => { - const jsonStr = '{"api_key":"sk-secret-123","name":"test"}' - const result = serializeAttribute(jsonStr, 10000) - expect(result).not.toContain("sk-secret-123") - expect(result).toContain("[REDACTED]") - }) - - it("redacts key=value patterns in strings", () => { - const cmdOutput = "config loaded: password=hunter2 host=localhost" - const result = serializeAttribute(cmdOutput, 10000) - expect(result).not.toContain("hunter2") - expect(result).toContain("[REDACTED]") - }) - - it("redacts multi-word bearer token values", () => { - const header = "Authorization: Bearer secret-token" - const result = serializeAttribute(header, 10000) - expect(result).not.toContain("secret-token") - expect(result).not.toContain("Bearer") - expect(result).toContain("[REDACTED]") - }) - - it("redacts header-style secrets with colons in tool output", () => { - const output = "HTTP/1.1 200 OK\nAuthorization: Bearer sk-abc123\nContent-Type: text/plain" - const result = serializeAttribute(output, 10000) - expect(result).not.toContain("sk-abc123") - expect(result).toContain("[REDACTED]") - expect(result).toContain("Content-Type") - }) - - it("truncates long output", () => { - const longStr = "a".repeat(200) - const result = serializeAttribute(longStr, 50) - expect(result).not.toBeNull() - expect(result!.length).toBeLessThan(200) - expect(result).toContain("...[truncated") - }) - - it("handles circular references", () => { - const obj: Record = { name: "test" } - obj.self = obj - const result = serializeAttribute(obj, 1000) - expect(result).toContain("[Circular]") - expect(result).toContain("test") - }) - - it("handles deep nesting", () => { - let obj: Record = { value: "deep" } - for (let i = 0; i < 20; i++) { - obj = { nested: obj } - } - const result = serializeAttribute(obj, 10000) - expect(result).toContain("[DepthLimit]") - }) - - it("returns null for undefined and null", () => { - expect(serializeAttribute(undefined, 1000)).toBeNull() - expect(serializeAttribute(null, 1000)).toBeNull() - }) +describe('serializeAttribute', () => { + it('serializes simple values', () => { + expect(serializeAttribute({ a: 1 }, 1000)).toBe('{"a":1}') + expect(serializeAttribute('hello', 1000)).toBe('hello') + }) + + it('redacts sensitive keys in objects', () => { + const input = { + command: 'curl', + api_key: 'sk-secret-123', + apiKey: 'another-secret', + token: 'my-token', + password: 'pass123', + authorization: 'Bearer xyz', + normal_field: 'visible', + } + const result = serializeAttribute(input, 10000) + expect(result).toContain('[REDACTED]') + expect(result).not.toContain('sk-secret-123') + expect(result).not.toContain('another-secret') + expect(result).not.toContain('my-token') + expect(result).not.toContain('pass123') + expect(result).not.toContain('Bearer xyz') + expect(result).toContain('curl') + expect(result).toContain('visible') + }) + + it('redacts nested sensitive keys', () => { + const input = { + headers: { Authorization: 'Bearer secret' }, + config: { api_key: 'hidden' }, + } + const result = serializeAttribute(input, 10000) + expect(result).not.toContain('secret') + expect(result).not.toContain('hidden') + }) + + it('redacts sensitive values embedded in strings', () => { + const jsonStr = '{"api_key":"sk-secret-123","name":"test"}' + const result = serializeAttribute(jsonStr, 10000) + expect(result).not.toContain('sk-secret-123') + expect(result).toContain('[REDACTED]') + }) + + it('redacts key=value patterns in strings', () => { + const cmdOutput = 'config loaded: password=hunter2 host=localhost' + const result = serializeAttribute(cmdOutput, 10000) + expect(result).not.toContain('hunter2') + expect(result).toContain('[REDACTED]') + }) + + it('redacts multi-word bearer token values', () => { + const header = 'Authorization: Bearer secret-token' + const result = serializeAttribute(header, 10000) + expect(result).not.toContain('secret-token') + expect(result).not.toContain('Bearer') + expect(result).toContain('[REDACTED]') + }) + + it('redacts header-style secrets with colons in tool output', () => { + const output = 'HTTP/1.1 200 OK\nAuthorization: Bearer sk-abc123\nContent-Type: text/plain' + const result = serializeAttribute(output, 10000) + expect(result).not.toContain('sk-abc123') + expect(result).toContain('[REDACTED]') + expect(result).toContain('Content-Type') + }) + + it('truncates long output', () => { + const longStr = 'a'.repeat(200) + const result = serializeAttribute(longStr, 50) + expect(result).not.toBeNull() + expect(result!.length).toBeLessThan(200) + expect(result).toContain('...[truncated') + }) + + it('handles circular references', () => { + const obj: Record = { name: 'test' } + obj.self = obj + const result = serializeAttribute(obj, 1000) + expect(result).toContain('[Circular]') + expect(result).toContain('test') + }) + + it('handles deep nesting', () => { + let obj: Record = { value: 'deep' } + for (let i = 0; i < 20; i++) { + obj = { nested: obj } + } + const result = serializeAttribute(obj, 10000) + expect(result).toContain('[DepthLimit]') + }) + + it('returns null for undefined and null', () => { + expect(serializeAttribute(undefined, 1000)).toBeNull() + expect(serializeAttribute(null, 1000)).toBeNull() + }) }) -describe("serializeError", () => { - it("serializes error objects to JSON", () => { - const error = { name: "UnknownError", data: { message: "boom" } } - const result = serializeError(error) - expect(result).toBe('{"name":"UnknownError","data":{"message":"boom"}}') - }) - - it("returns null for undefined", () => { - expect(serializeError(undefined)).toBeNull() - }) - - it("handles circular references via redaction", () => { - const circular: Record = { name: "BadError" } - circular.self = circular - const result = serializeError(circular as { name: string; data?: Record }) - expect(result).toContain("BadError") - expect(result).toContain("[Circular]") - }) - - it("redacts sensitive keys in error data", () => { - const error = { name: "AuthError", data: { api_key: "sk-secret-123", message: "failed" } } - const result = serializeError(error) - expect(result).not.toContain("sk-secret-123") - expect(result).toContain("[REDACTED]") - expect(result).toContain("failed") - }) +describe('serializeError', () => { + it('serializes error objects to JSON', () => { + const error = { name: 'UnknownError', data: { message: 'boom' } } + const result = serializeError(error) + expect(result).toBe('{"name":"UnknownError","data":{"message":"boom"}}') + }) + + it('returns null for undefined', () => { + expect(serializeError(undefined)).toBeNull() + }) + + it('handles circular references via redaction', () => { + const circular: Record = { name: 'BadError' } + circular.self = circular + const result = serializeError(circular as { name: string; data?: Record }) + expect(result).toContain('BadError') + expect(result).toContain('[Circular]') + }) + + it('redacts sensitive keys in error data', () => { + const error = { name: 'AuthError', data: { api_key: 'sk-secret-123', message: 'failed' } } + const result = serializeError(error) + expect(result).not.toContain('sk-secret-123') + expect(result).toContain('[REDACTED]') + expect(result).toContain('failed') + }) }) diff --git a/src/utils.ts b/src/utils.ts index dadf35d..0065ff4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,48 +1,45 @@ -import { hostname } from "node:os" -import { basename } from "node:path" -import type { PluginConfig } from "./types.js" +import { hostname } from 'node:os' +import { basename } from 'node:path' +import type { PluginConfig } from './types.js' export function loadConfig(): PluginConfig { - const tags: Record = {} - const tagsEnv = process.env.POSTHOG_TAGS - if (tagsEnv) { - for (const pair of tagsEnv.split(",")) { - const colonIdx = pair.indexOf(":") - if (colonIdx > 0) { - const key = pair.slice(0, colonIdx).trim() - const val = pair.slice(colonIdx + 1).trim() - if (key.length > 0 && val.length > 0) { - tags[key] = val + const tags: Record = {} + const tagsEnv = process.env.POSTHOG_TAGS + if (tagsEnv) { + for (const pair of tagsEnv.split(',')) { + const colonIdx = pair.indexOf(':') + if (colonIdx > 0) { + const key = pair.slice(0, colonIdx).trim() + const val = pair.slice(colonIdx + 1).trim() + if (key.length > 0 && val.length > 0) { + tags[key] = val + } + } } - } } - } - let distinctId = process.env.POSTHOG_DISTINCT_ID - if (!distinctId) { - try { - distinctId = hostname() - } catch { - distinctId = "opencode-user" + let distinctId = process.env.POSTHOG_DISTINCT_ID + if (!distinctId) { + try { + distinctId = hostname() + } catch { + distinctId = 'opencode-user' + } + } + + return { + apiKey: process.env.POSTHOG_API_KEY ?? '', + host: process.env.POSTHOG_HOST ?? 'https://us.i.posthog.com', + privacyMode: process.env.POSTHOG_PRIVACY_MODE === 'true', + enabled: process.env.POSTHOG_ENABLED !== 'false', + distinctId, + projectName: process.env.POSTHOG_PROJECT_NAME || basename(process.cwd()) || 'opencode-project', + tags, + maxAttributeLength: parseInt(process.env.POSTHOG_MAX_ATTRIBUTE_LENGTH ?? '12000', 10) || 12000, } - } - - return { - apiKey: process.env.POSTHOG_API_KEY ?? "", - host: process.env.POSTHOG_HOST ?? "https://us.i.posthog.com", - privacyMode: process.env.POSTHOG_PRIVACY_MODE === "true", - enabled: process.env.POSTHOG_ENABLED !== "false", - distinctId, - projectName: - process.env.POSTHOG_PROJECT_NAME || basename(process.cwd()) || "opencode-project", - tags, - maxAttributeLength: - parseInt(process.env.POSTHOG_MAX_ATTRIBUTE_LENGTH ?? "12000", 10) || 12000, - } } -const SENSITIVE_KEY_PATTERN = - /api[-_]?key|token|secret|password|authorization|credential|private[-_]?key/i +const SENSITIVE_KEY_PATTERN = /api[-_]?key|token|secret|password|authorization|credential|private[-_]?key/i /** * Patterns to detect sensitive values embedded in strings. @@ -51,7 +48,7 @@ const SENSITIVE_KEY_PATTERN = * Matches the full `"key":"value"` including the quoted value. */ const SENSITIVE_JSON_PATTERN = - /"(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)"\s*:\s*"[^"]*"/gi + /"(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)"\s*:\s*"[^"]*"/gi /** * Header/env-style: `Authorization: Bearer secret-token` or `password=hunter2` @@ -59,84 +56,72 @@ const SENSITIVE_JSON_PATTERN = * end-of-string so multi-word values like `Bearer xyz` are fully consumed. */ const SENSITIVE_KV_PATTERN = - /(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)\s*[=:]\s*[^\n,;]*/gi + /(?:api[-_]?key|token|secret|password|authorization|credential|private[-_]?key)\s*[=:]\s*[^\n,;]*/gi /** * Redact sensitive values found inline in a string. Handles both JSON-like * `"key":"value"` patterns and `key=value` / `key: value` patterns. */ function redactStringValues(str: string): string { - return str - .replace(SENSITIVE_JSON_PATTERN, "[REDACTED]") - .replace(SENSITIVE_KV_PATTERN, "[REDACTED]") + return str.replace(SENSITIVE_JSON_PATTERN, '[REDACTED]').replace(SENSITIVE_KV_PATTERN, '[REDACTED]') } -function redactSensitive( - value: unknown, - seen: WeakSet, - depth: number, -): unknown { - if (depth > 8) return "[DepthLimit]" - if (value === null || value === undefined) return value - if (typeof value === "string") return redactStringValues(value) - if (typeof value !== "object") return value - if (seen.has(value)) return "[Circular]" - seen.add(value) - - if (Array.isArray(value)) { - return value.map((item) => redactSensitive(item, seen, depth + 1)) - } - - const output: Record = {} - for (const [key, nested] of Object.entries(value)) { - if (SENSITIVE_KEY_PATTERN.test(key)) { - output[key] = "[REDACTED]" - } else { - output[key] = redactSensitive(nested, seen, depth + 1) +function redactSensitive(value: unknown, seen: WeakSet, depth: number): unknown { + if (depth > 8) return '[DepthLimit]' + if (value === null || value === undefined) return value + if (typeof value === 'string') return redactStringValues(value) + if (typeof value !== 'object') return value + if (seen.has(value)) return '[Circular]' + seen.add(value) + + if (Array.isArray(value)) { + return value.map((item) => redactSensitive(item, seen, depth + 1)) + } + + const output: Record = {} + for (const [key, nested] of Object.entries(value)) { + if (SENSITIVE_KEY_PATTERN.test(key)) { + output[key] = '[REDACTED]' + } else { + output[key] = redactSensitive(nested, seen, depth + 1) + } } - } - return output + return output } function truncate(value: string, maxLength: number): string { - if (maxLength <= 0) return "" - if (value.length <= maxLength) return value - const omitted = value.length - maxLength - return `${value.slice(0, maxLength)}...[truncated ${omitted} chars]` + if (maxLength <= 0) return '' + if (value.length <= maxLength) return value + const omitted = value.length - maxLength + return `${value.slice(0, maxLength)}...[truncated ${omitted} chars]` } -export function serializeAttribute( - value: unknown, - maxLength: number, -): string | null { - if (value === undefined || value === null) return null +export function serializeAttribute(value: unknown, maxLength: number): string | null { + if (value === undefined || value === null) return null - const redacted = redactSensitive(value, new WeakSet(), 0) + const redacted = redactSensitive(value, new WeakSet(), 0) - if (typeof redacted === "string") { - return truncate(redacted, maxLength) - } + if (typeof redacted === 'string') { + return truncate(redacted, maxLength) + } - try { - const json = JSON.stringify(redacted) - if (json === undefined) return null - return truncate(json, maxLength) - } catch { - return "[Unserializable]" - } + try { + const json = JSON.stringify(redacted) + if (json === undefined) return null + return truncate(json, maxLength) + } catch { + return '[Unserializable]' + } } -export function redactForPrivacy( - value: T, - privacyMode: boolean, -): T | null { - return privacyMode ? null : value +export function redactForPrivacy(value: T, privacyMode: boolean): T | null { + return privacyMode ? null : value } export function serializeError( - error: { name: string; data?: Record } | undefined, - maxLength: number = 12000, + error: { name: string; data?: Record } | undefined, + maxLength: number = 12000 ): string | null { - if (!error) return null - return serializeAttribute(error, maxLength) + if (!error) return null + return serializeAttribute(error, maxLength) } diff --git a/src/version.ts b/src/version.ts index 2dc6a32..39f77e5 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1,6 @@ -export const VERSION = "0.0.1" +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) +const pkg = require('../package.json') as { version: string } + +export const VERSION: string = pkg.version diff --git a/tsconfig.json b/tsconfig.json index 427fd43..d6effd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true, - "declaration": true, - "isolatedModules": true, - "resolveJsonModule": true - }, - "include": ["src"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "declaration": true, + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["src"] } From d63a62fe4c833759667cc476a80a5b4089437fcf Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 19:55:28 +0200 Subject: [PATCH 14/19] update README install instructions to match OpenCode plugin docs --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5037f09..78d4d64 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,28 @@ PostHog LLM Analytics plugin for [OpenCode](https://opencode.ai). Captures LLM g ## Installation -Add to your `opencode.json`: +Add `opencode-posthog` to your `opencode.json`: ```json { + "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-posthog"] } ``` -For local development, clone or symlink this repo into `.opencode/plugins/` and ensure `posthog-node` is installed (add it to `.opencode/package.json`). +The package is installed automatically at startup and cached in `~/.cache/opencode/node_modules/`. + +### Local development + +Place the plugin source in your project's `.opencode/plugins/` directory (or `~/.config/opencode/plugins/` for global use). Add `posthog-node` to `.opencode/package.json` so OpenCode installs it at startup: + +```json +{ + "dependencies": { + "posthog-node": "^5.0.0" + } +} +``` ## Configuration From 9c2358f9ac6b8d4e315a08fe989732a811020a59 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 20:02:49 +0200 Subject: [PATCH 15/19] switch from pnpm to bun OpenCode runs under Bun, so align the toolchain. Replace pnpm with bun install/lockfile, update CI setup action to use oven-sh/setup-bun, remove .nvmrc and packageManager field, simplify version.ts to use a direct JSON import instead of createRequire. --- .github/actions/setup/action.yml | 23 +- .nvmrc | 1 - bun.lock | 335 +++++++ package.json | 1 - pnpm-lock.yaml | 1526 ------------------------------ src/version.ts | 5 +- 6 files changed, 343 insertions(+), 1548 deletions(-) delete mode 100644 .nvmrc create mode 100644 bun.lock delete mode 100644 pnpm-lock.yaml diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index a03d810..afcaf22 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -32,28 +32,19 @@ inputs: runs: using: composite steps: - - name: Setup pnpm - uses: pnpm/action-setup@c336a2788d9774dccfdeb4823a5058ccc9f07453 # v4.3.0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: 'pnpm' + - name: Setup Bun + uses: oven-sh/setup-bun@v2 - name: Cache dependencies - id: pnpm-cache uses: actions/cache@v4 with: - path: | - **/node_modules - .pnpm-store - key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} + path: '**/node_modules' + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} restore-keys: | - ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} - ${{ runner.os }}-pnpm- + ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + ${{ runner.os }}-bun- - name: Install dependencies if: inputs.install == 'true' - run: pnpm install --frozen-lockfile + run: bun install --frozen-lockfile shell: bash diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2bd5a0a..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2fb986d --- /dev/null +++ b/bun.lock @@ -0,0 +1,335 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "opencode-posthog", + "dependencies": { + "posthog-node": "^5.0.0", + }, + "devDependencies": { + "@opencode-ai/plugin": "*", + "@opencode-ai/sdk": "*", + "@types/node": "^22.0.0", + "oxfmt": "^0.40.0", + "oxlint": "^1.55.0", + "typescript": "^5.8.0", + "vitest": "^3.0.0", + }, + "peerDependencies": { + "@opencode-ai/plugin": "*", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.3.15", "", { "dependencies": { "@opencode-ai/sdk": "1.3.15", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.96", "@opentui/solid": ">=0.1.96" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-jZJbuvUXc5Limz8pacQl+ffATjjKGlq+xaA4wTUeW+/spwOf7Yr5Ryyvan8eNlYM8wy6h5SLfznl1rlFpjYC8w=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-Uk59C7wsK20wpdr277yx7Xz7TqG5jGqlZUpSW3wDH/7a2K2iBg0lXc2wskHuCXLRXMhXpPZtb4a3SOpPENkkbg=="], + + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-S6zd5r1w/HmqR8t0CTnGjFTBLDq2QKORPwriCHxo4xFNuhmOTABGjPaNvCJJVnrKBLsohOeiDX3YqQfJPF+FXw=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-/mbS9UUP/5Vbl2D6osIdcYiP0oie63LKMoTyGj5hyMCK/SFkl3EhtyRAfdjPvuvHC0SXdW6ePaTKkBSq1SNcIw=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wRt8fRdfLiEhnRMBonlIbKrJWixoEmn6KCjKE9PElnrSDSXETGZfPb8ee+nQNTobXkCVvVLytp2o0obAsxl78Q=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fzowhqbOE/NRy+AE5ob0+Y4X243WbWzDb00W+pKwD7d9tOqsAFbtWUwIyqqCoCLxj791m2xXIEeLH/3uz7zCCg=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-agZ9ITaqdBjcerRRFEHB8s0OyVcQW8F9ZxsszjxzeSthQ4fcN2MuOtQFWec1ed8/lDa50jSLHVE2/xPmTgtCfQ=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZM2oQ47p28TP1DVIp7HL1QoMUgqlBFHey0ksHct7tMXoU5BqjNvPWw7888azzMt25lnyPODVuye1wvNbvVUFOA=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RBFPAxRAIsMisKM47Oe6Lwdv6agZYLz02CUhVCD1sOv5ajAcRMrnwCFBPWwGXpazToW2mjnZxFos8TuFjTU15A=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nb2XbQ+wV3W2jSIihXdPj7k83eOxeSgYP3N/SRXvQ6ZYPIk6Q86qEh5Gl/7OitX3bQoQrESqm1yMLvZV8/J7dA=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tGmWhLD/0YMotCdfezlT6tC/MJG/wKpo4vnQ3Cq+4eBk/BwNv7EmkD0VkD5F/dYkT3b8FNU01X2e8vvJuWoM1w=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rVbFyM3e7YhkVnp0IVYjaSHfrBWcTRWb60LEcdNAJcE2mbhTpbqKufx0FrhWfoxOrW/+7UJonAOShoFFLigDqQ=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-3ZqBw14JtWeEoLiioJcXSJz8RQyPE+3jLARnYM1HdPzZG4vk+Ua8CUupt2+d+vSAvMyaQBTN2dZK+kbBS/j5mA=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-JJ4PPSdcbGBjPvb+O7xYm2FmAsKCyuEMYhqatBAHMp/6TA6rVlf9Z/sYPa4/3Bommb+8nndm15SPFRHEPU5qFA=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Kp0zNJoX9Ik77wUya2tpBY3W9f40VUoMQLWVaob5SgCrblH/t2xr/9B2bWHfs0WCefuGmqXcB+t0Lq77sbBmZw=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7YTCNzleWTaQTqNGUNQ66qVjpoV6DjbCOea+RnpMBly2bpzrI/uu7Rr+2zcgRfNxyjXaFTVQKaRKjqVdeUfeVA=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hWnSzJ0oegeOwfOEeejYXfBqmnRGHusgtHfCPzmvJvHTwy1s3Neo59UKc1CmpE3zxvrCzJoVHos0rr97GHMNPw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.40.0", "", { "os": "none", "cpu": "arm64" }, "sha512-28sJC1lR4qtBJGzSRRbPnSW3GxU2+4YyQFE6rCmsUYqZ5XYH8jg0/w+CvEzQ8TuAQz5zLkcA25nFQGwoU0PT3Q=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-cDkRnyT0dqwF5oIX1Cv59HKCeZQFbWWdUpXa3uvnHFT2iwYSSZspkhgjXjU6iDp5pFPaAEAe9FIbMoTgkTmKPg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-7rPemBJjqm5Gkv6ZRCPvK8lE6AqQ/2z31DRdWazyx2ZvaSgL7QGofHXHNouRpPvNsT9yxRNQJgigsWkc+0qg4w=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg=="], + + "@posthog/core": ["@posthog/core@1.24.6", "", {}, "sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], + + "oxlint": ["oxlint@1.58.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.58.0", "@oxlint/binding-android-arm64": "1.58.0", "@oxlint/binding-darwin-arm64": "1.58.0", "@oxlint/binding-darwin-x64": "1.58.0", "@oxlint/binding-freebsd-x64": "1.58.0", "@oxlint/binding-linux-arm-gnueabihf": "1.58.0", "@oxlint/binding-linux-arm-musleabihf": "1.58.0", "@oxlint/binding-linux-arm64-gnu": "1.58.0", "@oxlint/binding-linux-arm64-musl": "1.58.0", "@oxlint/binding-linux-ppc64-gnu": "1.58.0", "@oxlint/binding-linux-riscv64-gnu": "1.58.0", "@oxlint/binding-linux-riscv64-musl": "1.58.0", "@oxlint/binding-linux-s390x-gnu": "1.58.0", "@oxlint/binding-linux-x64-gnu": "1.58.0", "@oxlint/binding-linux-x64-musl": "1.58.0", "@oxlint/binding-openharmony-arm64": "1.58.0", "@oxlint/binding-win32-arm64-msvc": "1.58.0", "@oxlint/binding-win32-ia32-msvc": "1.58.0", "@oxlint/binding-win32-x64-msvc": "1.58.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "posthog-node": ["posthog-node@5.28.11", "", { "dependencies": { "@posthog/core": "1.24.6" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-H4FOiqKUBO8SVXyXlU5tyifeS11hyTGVwBirFPR5rPtw8X6OFs5xVLx38YL7ZBLjaa9u8is+nIWXKBwWsZ2vlw=="], + + "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + } +} diff --git a/package.json b/package.json index e3305a5..2ad90ef 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "license": "MIT", "type": "module", "main": "src/index.ts", - "packageManager": "pnpm@10.12.1", "files": [ "src", "!src/**/*.test.ts" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 16120c5..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1526 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - posthog-node: - specifier: ^5.0.0 - version: 5.28.11 - devDependencies: - '@opencode-ai/plugin': - specifier: '*' - version: 1.3.15 - '@opencode-ai/sdk': - specifier: '*' - version: 1.3.15 - '@types/node': - specifier: ^22.0.0 - version: 22.19.17 - oxfmt: - specifier: ^0.40.0 - version: 0.40.0 - oxlint: - specifier: ^1.55.0 - version: 1.58.0 - typescript: - specifier: ^5.8.0 - version: 5.9.3 - vitest: - specifier: ^3.0.0 - version: 3.2.4(@types/node@22.19.17) - -packages: - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@opencode-ai/plugin@1.3.15': - resolution: {integrity: sha512-jZJbuvUXc5Limz8pacQl+ffATjjKGlq+xaA4wTUeW+/spwOf7Yr5Ryyvan8eNlYM8wy6h5SLfznl1rlFpjYC8w==} - peerDependencies: - '@opentui/core': '>=0.1.96' - '@opentui/solid': '>=0.1.96' - peerDependenciesMeta: - '@opentui/core': - optional: true - '@opentui/solid': - optional: true - - '@opencode-ai/sdk@1.3.15': - resolution: {integrity: sha512-Uk59C7wsK20wpdr277yx7Xz7TqG5jGqlZUpSW3wDH/7a2K2iBg0lXc2wskHuCXLRXMhXpPZtb4a3SOpPENkkbg==} - - '@oxfmt/binding-android-arm-eabi@0.40.0': - resolution: {integrity: sha512-S6zd5r1w/HmqR8t0CTnGjFTBLDq2QKORPwriCHxo4xFNuhmOTABGjPaNvCJJVnrKBLsohOeiDX3YqQfJPF+FXw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxfmt/binding-android-arm64@0.40.0': - resolution: {integrity: sha512-/mbS9UUP/5Vbl2D6osIdcYiP0oie63LKMoTyGj5hyMCK/SFkl3EhtyRAfdjPvuvHC0SXdW6ePaTKkBSq1SNcIw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxfmt/binding-darwin-arm64@0.40.0': - resolution: {integrity: sha512-wRt8fRdfLiEhnRMBonlIbKrJWixoEmn6KCjKE9PElnrSDSXETGZfPb8ee+nQNTobXkCVvVLytp2o0obAsxl78Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxfmt/binding-darwin-x64@0.40.0': - resolution: {integrity: sha512-fzowhqbOE/NRy+AE5ob0+Y4X243WbWzDb00W+pKwD7d9tOqsAFbtWUwIyqqCoCLxj791m2xXIEeLH/3uz7zCCg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxfmt/binding-freebsd-x64@0.40.0': - resolution: {integrity: sha512-agZ9ITaqdBjcerRRFEHB8s0OyVcQW8F9ZxsszjxzeSthQ4fcN2MuOtQFWec1ed8/lDa50jSLHVE2/xPmTgtCfQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxfmt/binding-linux-arm-gnueabihf@0.40.0': - resolution: {integrity: sha512-ZM2oQ47p28TP1DVIp7HL1QoMUgqlBFHey0ksHct7tMXoU5BqjNvPWw7888azzMt25lnyPODVuye1wvNbvVUFOA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm-musleabihf@0.40.0': - resolution: {integrity: sha512-RBFPAxRAIsMisKM47Oe6Lwdv6agZYLz02CUhVCD1sOv5ajAcRMrnwCFBPWwGXpazToW2mjnZxFos8TuFjTU15A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm64-gnu@0.40.0': - resolution: {integrity: sha512-Nb2XbQ+wV3W2jSIihXdPj7k83eOxeSgYP3N/SRXvQ6ZYPIk6Q86qEh5Gl/7OitX3bQoQrESqm1yMLvZV8/J7dA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxfmt/binding-linux-arm64-musl@0.40.0': - resolution: {integrity: sha512-tGmWhLD/0YMotCdfezlT6tC/MJG/wKpo4vnQ3Cq+4eBk/BwNv7EmkD0VkD5F/dYkT3b8FNU01X2e8vvJuWoM1w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxfmt/binding-linux-ppc64-gnu@0.40.0': - resolution: {integrity: sha512-rVbFyM3e7YhkVnp0IVYjaSHfrBWcTRWb60LEcdNAJcE2mbhTpbqKufx0FrhWfoxOrW/+7UJonAOShoFFLigDqQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - - '@oxfmt/binding-linux-riscv64-gnu@0.40.0': - resolution: {integrity: sha512-3ZqBw14JtWeEoLiioJcXSJz8RQyPE+3jLARnYM1HdPzZG4vk+Ua8CUupt2+d+vSAvMyaQBTN2dZK+kbBS/j5mA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxfmt/binding-linux-riscv64-musl@0.40.0': - resolution: {integrity: sha512-JJ4PPSdcbGBjPvb+O7xYm2FmAsKCyuEMYhqatBAHMp/6TA6rVlf9Z/sYPa4/3Bommb+8nndm15SPFRHEPU5qFA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxfmt/binding-linux-s390x-gnu@0.40.0': - resolution: {integrity: sha512-Kp0zNJoX9Ik77wUya2tpBY3W9f40VUoMQLWVaob5SgCrblH/t2xr/9B2bWHfs0WCefuGmqXcB+t0Lq77sbBmZw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - - '@oxfmt/binding-linux-x64-gnu@0.40.0': - resolution: {integrity: sha512-7YTCNzleWTaQTqNGUNQ66qVjpoV6DjbCOea+RnpMBly2bpzrI/uu7Rr+2zcgRfNxyjXaFTVQKaRKjqVdeUfeVA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxfmt/binding-linux-x64-musl@0.40.0': - resolution: {integrity: sha512-hWnSzJ0oegeOwfOEeejYXfBqmnRGHusgtHfCPzmvJvHTwy1s3Neo59UKc1CmpE3zxvrCzJoVHos0rr97GHMNPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxfmt/binding-openharmony-arm64@0.40.0': - resolution: {integrity: sha512-28sJC1lR4qtBJGzSRRbPnSW3GxU2+4YyQFE6rCmsUYqZ5XYH8jg0/w+CvEzQ8TuAQz5zLkcA25nFQGwoU0PT3Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxfmt/binding-win32-arm64-msvc@0.40.0': - resolution: {integrity: sha512-cDkRnyT0dqwF5oIX1Cv59HKCeZQFbWWdUpXa3uvnHFT2iwYSSZspkhgjXjU6iDp5pFPaAEAe9FIbMoTgkTmKPg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxfmt/binding-win32-ia32-msvc@0.40.0': - resolution: {integrity: sha512-7rPemBJjqm5Gkv6ZRCPvK8lE6AqQ/2z31DRdWazyx2ZvaSgL7QGofHXHNouRpPvNsT9yxRNQJgigsWkc+0qg4w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxfmt/binding-win32-x64-msvc@0.40.0': - resolution: {integrity: sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxlint/binding-android-arm-eabi@1.58.0': - resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxlint/binding-android-arm64@1.58.0': - resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxlint/binding-darwin-arm64@1.58.0': - resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxlint/binding-darwin-x64@1.58.0': - resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxlint/binding-freebsd-x64@1.58.0': - resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxlint/binding-linux-arm-gnueabihf@1.58.0': - resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm-musleabihf@1.58.0': - resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm64-gnu@1.58.0': - resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxlint/binding-linux-arm64-musl@1.58.0': - resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxlint/binding-linux-ppc64-gnu@1.58.0': - resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - - '@oxlint/binding-linux-riscv64-gnu@1.58.0': - resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxlint/binding-linux-riscv64-musl@1.58.0': - resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxlint/binding-linux-s390x-gnu@1.58.0': - resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - - '@oxlint/binding-linux-x64-gnu@1.58.0': - resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxlint/binding-linux-x64-musl@1.58.0': - resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxlint/binding-openharmony-arm64@1.58.0': - resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxlint/binding-win32-arm64-msvc@1.58.0': - resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxlint/binding-win32-ia32-msvc@1.58.0': - resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxlint/binding-win32-x64-msvc@1.58.0': - resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@posthog/core@1.24.6': - resolution: {integrity: sha512-9WkcRKqmXSWIJcca6m3VwA9YbFd4HiG2hKEtDq6FcwEHlvfDhQQUZ5/sJZ47Fw8OtyNMHQ6rW4+COttk4Bg5NQ==} - - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} - cpu: [x64] - os: [win32] - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/node@22.19.17': - resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} - - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - oxfmt@0.40.0: - resolution: {integrity: sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - oxlint@1.58.0: - resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - oxlint-tsgolint: '>=0.18.0' - peerDependenciesMeta: - oxlint-tsgolint: - optional: true - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - posthog-node@5.28.11: - resolution: {integrity: sha512-H4FOiqKUBO8SVXyXlU5tyifeS11hyTGVwBirFPR5rPtw8X6OFs5xVLx38YL7ZBLjaa9u8is+nIWXKBwWsZ2vlw==} - engines: {node: ^20.20.0 || >=22.22.0} - peerDependencies: - rxjs: ^7.0.0 - peerDependenciesMeta: - rxjs: - optional: true - - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinypool@2.1.0: - resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} - engines: {node: ^20.0.0 || >=22.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - zod@4.1.8: - resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} - -snapshots: - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@opencode-ai/plugin@1.3.15': - dependencies: - '@opencode-ai/sdk': 1.3.15 - zod: 4.1.8 - - '@opencode-ai/sdk@1.3.15': - dependencies: - cross-spawn: 7.0.6 - - '@oxfmt/binding-android-arm-eabi@0.40.0': - optional: true - - '@oxfmt/binding-android-arm64@0.40.0': - optional: true - - '@oxfmt/binding-darwin-arm64@0.40.0': - optional: true - - '@oxfmt/binding-darwin-x64@0.40.0': - optional: true - - '@oxfmt/binding-freebsd-x64@0.40.0': - optional: true - - '@oxfmt/binding-linux-arm-gnueabihf@0.40.0': - optional: true - - '@oxfmt/binding-linux-arm-musleabihf@0.40.0': - optional: true - - '@oxfmt/binding-linux-arm64-gnu@0.40.0': - optional: true - - '@oxfmt/binding-linux-arm64-musl@0.40.0': - optional: true - - '@oxfmt/binding-linux-ppc64-gnu@0.40.0': - optional: true - - '@oxfmt/binding-linux-riscv64-gnu@0.40.0': - optional: true - - '@oxfmt/binding-linux-riscv64-musl@0.40.0': - optional: true - - '@oxfmt/binding-linux-s390x-gnu@0.40.0': - optional: true - - '@oxfmt/binding-linux-x64-gnu@0.40.0': - optional: true - - '@oxfmt/binding-linux-x64-musl@0.40.0': - optional: true - - '@oxfmt/binding-openharmony-arm64@0.40.0': - optional: true - - '@oxfmt/binding-win32-arm64-msvc@0.40.0': - optional: true - - '@oxfmt/binding-win32-ia32-msvc@0.40.0': - optional: true - - '@oxfmt/binding-win32-x64-msvc@0.40.0': - optional: true - - '@oxlint/binding-android-arm-eabi@1.58.0': - optional: true - - '@oxlint/binding-android-arm64@1.58.0': - optional: true - - '@oxlint/binding-darwin-arm64@1.58.0': - optional: true - - '@oxlint/binding-darwin-x64@1.58.0': - optional: true - - '@oxlint/binding-freebsd-x64@1.58.0': - optional: true - - '@oxlint/binding-linux-arm-gnueabihf@1.58.0': - optional: true - - '@oxlint/binding-linux-arm-musleabihf@1.58.0': - optional: true - - '@oxlint/binding-linux-arm64-gnu@1.58.0': - optional: true - - '@oxlint/binding-linux-arm64-musl@1.58.0': - optional: true - - '@oxlint/binding-linux-ppc64-gnu@1.58.0': - optional: true - - '@oxlint/binding-linux-riscv64-gnu@1.58.0': - optional: true - - '@oxlint/binding-linux-riscv64-musl@1.58.0': - optional: true - - '@oxlint/binding-linux-s390x-gnu@1.58.0': - optional: true - - '@oxlint/binding-linux-x64-gnu@1.58.0': - optional: true - - '@oxlint/binding-linux-x64-musl@1.58.0': - optional: true - - '@oxlint/binding-openharmony-arm64@1.58.0': - optional: true - - '@oxlint/binding-win32-arm64-msvc@1.58.0': - optional: true - - '@oxlint/binding-win32-ia32-msvc@1.58.0': - optional: true - - '@oxlint/binding-win32-x64-msvc@1.58.0': - optional: true - - '@posthog/core@1.24.6': {} - - '@rollup/rollup-android-arm-eabi@4.60.1': - optional: true - - '@rollup/rollup-android-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-x64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.1': - optional: true - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.8': {} - - '@types/node@22.19.17': - dependencies: - undici-types: 6.21.0 - - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.17))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@22.19.17) - - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.1.0 - - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 - - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - - assertion-error@2.0.1: {} - - cac@6.7.14: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - expect-type@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fsevents@2.3.3: - optional: true - - isexe@2.0.0: {} - - js-tokens@9.0.1: {} - - loupe@3.2.1: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - oxfmt@0.40.0: - dependencies: - tinypool: 2.1.0 - optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.40.0 - '@oxfmt/binding-android-arm64': 0.40.0 - '@oxfmt/binding-darwin-arm64': 0.40.0 - '@oxfmt/binding-darwin-x64': 0.40.0 - '@oxfmt/binding-freebsd-x64': 0.40.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.40.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.40.0 - '@oxfmt/binding-linux-arm64-gnu': 0.40.0 - '@oxfmt/binding-linux-arm64-musl': 0.40.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.40.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.40.0 - '@oxfmt/binding-linux-riscv64-musl': 0.40.0 - '@oxfmt/binding-linux-s390x-gnu': 0.40.0 - '@oxfmt/binding-linux-x64-gnu': 0.40.0 - '@oxfmt/binding-linux-x64-musl': 0.40.0 - '@oxfmt/binding-openharmony-arm64': 0.40.0 - '@oxfmt/binding-win32-arm64-msvc': 0.40.0 - '@oxfmt/binding-win32-ia32-msvc': 0.40.0 - '@oxfmt/binding-win32-x64-msvc': 0.40.0 - - oxlint@1.58.0: - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.58.0 - '@oxlint/binding-android-arm64': 1.58.0 - '@oxlint/binding-darwin-arm64': 1.58.0 - '@oxlint/binding-darwin-x64': 1.58.0 - '@oxlint/binding-freebsd-x64': 1.58.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.58.0 - '@oxlint/binding-linux-arm-musleabihf': 1.58.0 - '@oxlint/binding-linux-arm64-gnu': 1.58.0 - '@oxlint/binding-linux-arm64-musl': 1.58.0 - '@oxlint/binding-linux-ppc64-gnu': 1.58.0 - '@oxlint/binding-linux-riscv64-gnu': 1.58.0 - '@oxlint/binding-linux-riscv64-musl': 1.58.0 - '@oxlint/binding-linux-s390x-gnu': 1.58.0 - '@oxlint/binding-linux-x64-gnu': 1.58.0 - '@oxlint/binding-linux-x64-musl': 1.58.0 - '@oxlint/binding-openharmony-arm64': 1.58.0 - '@oxlint/binding-win32-arm64-msvc': 1.58.0 - '@oxlint/binding-win32-ia32-msvc': 1.58.0 - '@oxlint/binding-win32-x64-msvc': 1.58.0 - - path-key@3.1.1: {} - - pathe@2.0.3: {} - - pathval@2.0.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - posthog-node@5.28.11: - dependencies: - '@posthog/core': 1.24.6 - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@3.10.0: {} - - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinypool@1.1.1: {} - - tinypool@2.1.0: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} - - typescript@5.9.3: {} - - undici-types@6.21.0: {} - - vite-node@3.2.4(@types/node@22.19.17): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.17) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.3.1(@types/node@22.19.17): - dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.8 - rollup: 4.60.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.19.17 - fsevents: 2.3.3 - - vitest@3.2.4(@types/node@22.19.17): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.17)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@22.19.17) - vite-node: 3.2.4(@types/node@22.19.17) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.19.17 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - zod@4.1.8: {} diff --git a/src/version.ts b/src/version.ts index 39f77e5..3c6253f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,6 +1,3 @@ -import { createRequire } from 'node:module' - -const require = createRequire(import.meta.url) -const pkg = require('../package.json') as { version: string } +import pkg from '../package.json' export const VERSION: string = pkg.version From 997063b6cf97f5d8157a00732e77bbc3b9d977f1 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 20:06:44 +0200 Subject: [PATCH 16/19] fix CI: replace pnpm with bun run in workflow steps --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c5f9e0..9e7b640 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,10 @@ jobs: uses: ./.github/actions/setup - name: Typecheck - run: pnpm typecheck + run: bun run typecheck - name: Lint - run: pnpm lint + run: bun run lint - name: Test - run: pnpm test + run: bun run test From 6aafb7103974893db1e5afb346abe80500394fa0 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 20:32:19 +0200 Subject: [PATCH 17/19] fix step-input ordering and cache token property names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Snapshot stepInputMessages at step-start into stepInputSnapshot. buildAiGeneration reads from the snapshot, so tool results from the current step don't leak into the same generation's $ai_input. Tool results are still appended to stepInputMessages for the next step's snapshot. 2. Rename $ai_cache_read_input_tokens → cache_read_input_tokens and $ai_cache_creation_input_tokens → cache_creation_input_tokens to match PostHog's LLM Analytics schema (and posthog-pi reference). --- src/events.test.ts | 14 ++++++++------ src/events.ts | 10 +++++----- src/index.ts | 6 ++++++ src/types.ts | 4 +++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/events.test.ts b/src/events.test.ts index 9535ea2..ee88adf 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -56,6 +56,7 @@ function makeTrace(overrides?: Partial): TraceState { userPrompt: 'Hello', lastAssistantText: 'Hi there!', stepInputMessages: [{ role: 'user', content: 'Hello' }], + stepInputSnapshot: [{ role: 'user', content: 'Hello' }], stepAssistantText: 'Hi there!', messageIds: new Set(), ...overrides, @@ -103,8 +104,8 @@ describe('buildAiGeneration', () => { expect(result.properties.$ai_input_tokens).toBe(100) expect(result.properties.$ai_output_tokens).toBe(50) expect(result.properties.$ai_reasoning_tokens).toBe(10) - expect(result.properties.$ai_cache_read_input_tokens).toBe(5) - expect(result.properties.$ai_cache_creation_input_tokens).toBe(3) + expect(result.properties.cache_read_input_tokens).toBe(5) + expect(result.properties.cache_creation_input_tokens).toBe(3) expect(result.properties.$ai_total_cost_usd).toBe(0.003) expect(result.properties.$ai_stop_reason).toBe('stop') expect(result.properties.$ai_is_error).toBe(false) @@ -130,10 +131,10 @@ describe('buildAiGeneration', () => { expect(typeof result.properties.$ai_span_id).toBe('string') }) - it('includes input and output content from step messages', () => { + it('includes input and output content from step snapshot', () => { const trace = makeTrace({ userPrompt: 'What is 2+2?', - stepInputMessages: [{ role: 'user', content: 'What is 2+2?' }], + stepInputSnapshot: [{ role: 'user', content: 'What is 2+2?' }], stepAssistantText: '4', lastAssistantText: '4', }) @@ -142,9 +143,10 @@ describe('buildAiGeneration', () => { expect(result.properties.$ai_output_choices).toEqual([{ role: 'assistant', content: '4' }]) }) - it('includes tool results in input for multi-step generations', () => { + it('includes prior tool results in input for multi-step generations', () => { const trace = makeTrace({ - stepInputMessages: [ + // Snapshot was taken at step-start, before current-step tools ran + stepInputSnapshot: [ { role: 'user', content: 'Read the file' }, { role: 'tool', content: '[read] file contents here' }, ], diff --git a/src/events.ts b/src/events.ts index 5fb2495..6d681bf 100644 --- a/src/events.ts +++ b/src/events.ts @@ -32,10 +32,10 @@ export function buildAiGeneration( // during this step already reference the correct parent. const spanId = trace.currentGenerationSpanId ?? randomUUID() - // Use accumulated step input messages (includes user prompt + tool results) - // for accurate per-roundtrip context. + // Use the snapshot taken at step-start, which contains context the model + // actually saw (user prompt + prior tool results), not current-step tools. const inputMessages = redactForPrivacy( - trace.stepInputMessages.length > 0 ? trace.stepInputMessages : null, + trace.stepInputSnapshot.length > 0 ? trace.stepInputSnapshot : null, config.privacyMode ) @@ -57,8 +57,8 @@ export function buildAiGeneration( $ai_input_tokens: part.tokens.input, $ai_output_tokens: part.tokens.output, $ai_reasoning_tokens: part.tokens.reasoning, - $ai_cache_read_input_tokens: part.tokens.cache.read, - $ai_cache_creation_input_tokens: part.tokens.cache.write, + cache_read_input_tokens: part.tokens.cache.read, + cache_creation_input_tokens: part.tokens.cache.write, $ai_total_cost_usd: part.cost, $ai_stop_reason: mapStopReason(part.reason), diff --git a/src/index.ts b/src/index.ts index 6a273ca..16e2dd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,7 @@ export const PostHogPlugin: Plugin = async () => { totalCost: 0, hadError: false, stepInputMessages: [], + stepInputSnapshot: [], messageIds: new Set(), } traces.set(sessionId, trace) @@ -81,6 +82,7 @@ export const PostHogPlugin: Plugin = async () => { hadError: false, agentName: msg.agent, stepInputMessages: [], + stepInputSnapshot: [], messageIds: new Set([msg.id]), } traces.set(msg.sessionID, trace) @@ -150,6 +152,10 @@ export const PostHogPlugin: Plugin = async () => { // Allocate the generation span ID eagerly so that tool spans // emitted during this step can reference it as their parent. trace.currentGenerationSpanId = randomUUID() + // Snapshot current input messages before tools run, so the + // generation reports only what the model saw as input, not + // the tool results from the current step. + trace.stepInputSnapshot = [...trace.stepInputMessages] // Reset per-step assistant text for the new generation trace.stepAssistantText = undefined } diff --git a/src/types.ts b/src/types.ts index 79d7bea..0d9d611 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,8 +25,10 @@ export interface TraceState { lastError?: string userPrompt?: string lastAssistantText?: string - /** Accumulated input context for the current step (user prompt + tool results). */ + /** Accumulated input context across steps (user prompt + tool results from prior steps). */ stepInputMessages: InputMessage[] + /** Snapshot of stepInputMessages taken at step-start, used as $ai_input for the generation. */ + stepInputSnapshot: InputMessage[] /** Assistant text accumulated during the current step, reset on each step-start. */ stepAssistantText?: string currentAssistantMsg?: AssistantInfo From df16bf97cee854d139cb983121865fdda0251eb7 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 20:33:18 +0200 Subject: [PATCH 18/19] configure package.json and tsconfig for npm publishing Add exports map, module/types fields, build script (bun build), prepublishOnly hook, repository/bugs/homepage URLs. Update tsconfig with outDir/declarationDir/rootDir and bun-types. Replace @types/node with @types/bun to match the runtime. --- bun.lock | 6 +++++- package.json | 24 ++++++++++++++++++++++-- tsconfig.json | 9 +++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 2fb986d..8b82351 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "devDependencies": { "@opencode-ai/plugin": "*", "@opencode-ai/sdk": "*", - "@types/node": "^22.0.0", + "@types/bun": "^1.3.0", "oxfmt": "^0.40.0", "oxlint": "^1.55.0", "typescript": "^5.8.0", @@ -208,6 +208,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -232,6 +234,8 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], diff --git a/package.json b/package.json index 2ad90ef..5db0f2a 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,18 @@ "author": "Nejc Drobnič ", "license": "MIT", "type": "module", - "main": "src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, "files": [ + "dist", "src", "!src/**/*.test.ts" ], @@ -19,7 +29,17 @@ "tracing", "analytics" ], + "repository": { + "type": "git", + "url": "git+https://github.com/Quantumlyy/opencode-posthog.git" + }, + "bugs": { + "url": "https://github.com/Quantumlyy/opencode-posthog/issues" + }, + "homepage": "https://github.com/Quantumlyy/opencode-posthog#readme", "scripts": { + "build": "bun build src/index.ts --outdir dist --target node", + "prepublishOnly": "bun run build", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", @@ -35,7 +55,7 @@ "devDependencies": { "@opencode-ai/plugin": "*", "@opencode-ai/sdk": "*", - "@types/node": "^22.0.0", + "@types/bun": "^1.3.0", "oxfmt": "^0.40.0", "oxlint": "^1.55.0", "typescript": "^5.8.0", diff --git a/tsconfig.json b/tsconfig.json index d6effd4..a65b2ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,13 @@ "skipLibCheck": true, "noEmit": true, "declaration": true, + "declarationDir": "./dist", + "outDir": "./dist", + "rootDir": "./src", "isolatedModules": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "types": ["bun-types"] }, - "include": ["src"] + "include": ["src"], + "exclude": ["node_modules", "dist"] } From 63b58df4ad0552da850afa9a0687c1ed4a9c5468 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sun, 5 Apr 2026 20:33:54 +0200 Subject: [PATCH 19/19] only ship dist in npm package --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 5db0f2a..ac628e4 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,7 @@ } }, "files": [ - "dist", - "src", - "!src/**/*.test.ts" + "dist" ], "keywords": [ "opencode-plugin",