diff --git a/.gitignore b/.gitignore index ec8b0b2..f1f5836 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ Thumbs.db # OpenCode .opencode/ + +# Tests (local development only) +tests/ diff --git a/README.md b/README.md index 323bf91..7fcc1fe 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ If you want to ensure a specific version is always used or update your version, ```json { "plugin": [ - "@tarquinen/opencode-dcp@0.3.10" + "@tarquinen/opencode-dcp@0.3.12" ] } ``` diff --git a/index.ts b/index.ts index 2e33359..fc2e772 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import type { Plugin } from "@opencode-ai/plugin" import { getConfig } from "./lib/config" import { Logger } from "./lib/logger" import { Janitor, type SessionStats } from "./lib/janitor" +import { checkForUpdates } from "./lib/version-checker" /** * Checks if a session is a subagent (child session) @@ -145,6 +146,9 @@ const plugin: Plugin = (async (ctx) => { model: config.model || "auto" }) + // Check for updates on launch (fire and forget) + checkForUpdates(ctx.client).catch(() => {}) + return { /** * Event Hook: Triggers janitor analysis when session becomes idle diff --git a/lib/deduplicator.ts b/lib/deduplicator.ts index 7fc7a74..15767b5 100644 --- a/lib/deduplicator.ts +++ b/lib/deduplicator.ts @@ -27,35 +27,35 @@ export function detectDuplicates( protectedTools: string[] ): DuplicateDetectionResult { const signatureMap = new Map() - + // Filter out protected tools before processing const deduplicatableIds = unprunedToolCallIds.filter(id => { const metadata = toolMetadata.get(id) return !metadata || !protectedTools.includes(metadata.tool) }) - + // Build map of signature -> [ids in chronological order] for (const id of deduplicatableIds) { const metadata = toolMetadata.get(id) if (!metadata) continue - + const signature = createToolSignature(metadata.tool, metadata.parameters) if (!signatureMap.has(signature)) { signatureMap.set(signature, []) } signatureMap.get(signature)!.push(id) } - + // Identify duplicates (keep only last occurrence) const duplicateIds: string[] = [] const deduplicationDetails = new Map() - + for (const [signature, ids] of signatureMap.entries()) { if (ids.length > 1) { const metadata = toolMetadata.get(ids[0])! const idsToRemove = ids.slice(0, -1) // All except last duplicateIds.push(...idsToRemove) - + deduplicationDetails.set(signature, { toolName: metadata.tool, parameterKey: extractParameterKey(metadata), @@ -65,7 +65,7 @@ export function detectDuplicates( }) } } - + return { duplicateIds, deduplicationDetails } } @@ -75,7 +75,7 @@ export function detectDuplicates( */ function createToolSignature(tool: string, parameters?: any): string { if (!parameters) return tool - + // Normalize parameters for consistent comparison const normalized = normalizeParameters(parameters) const sorted = sortObjectKeys(normalized) @@ -91,7 +91,7 @@ function createToolSignature(tool: string, parameters?: any): string { function normalizeParameters(params: any): any { if (typeof params !== 'object' || params === null) return params if (Array.isArray(params)) return params - + const normalized: any = {} for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { @@ -107,7 +107,7 @@ function normalizeParameters(params: any): any { function sortObjectKeys(obj: any): any { if (typeof obj !== 'object' || obj === null) return obj if (Array.isArray(obj)) return obj.map(sortObjectKeys) - + const sorted: any = {} for (const key of Object.keys(obj).sort()) { sorted[key] = sortObjectKeys(obj[key]) @@ -136,9 +136,9 @@ function sortObjectKeys(obj: any): any { */ export function extractParameterKey(metadata: { tool: string, parameters?: any }): string { if (!metadata.parameters) return '' - + const { tool, parameters } = metadata - + // ===== File Operation Tools ===== if (tool === "read" && parameters.filePath) { return parameters.filePath @@ -149,7 +149,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } if (tool === "edit" && parameters.filePath) { return parameters.filePath } - + // ===== Directory/Search Tools ===== if (tool === "list") { // path is optional, defaults to current directory @@ -170,17 +170,17 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } } return '(unknown pattern)' } - + // ===== Execution Tools ===== if (tool === "bash") { if (parameters.description) return parameters.description if (parameters.command) { - return parameters.command.length > 50 + return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command } } - + // ===== Web Tools ===== if (tool === "webfetch" && parameters.url) { return parameters.url @@ -191,7 +191,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } if (tool === "codesearch" && parameters.query) { return `"${parameters.query}"` } - + // ===== Todo Tools ===== // Note: Todo tools are stateful and in protectedTools by default if (tool === "todowrite") { @@ -200,7 +200,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } if (tool === "todoread") { return "read todo list" } - + // ===== Agent/Task Tools ===== // Note: task is in protectedTools by default if (tool === "task" && parameters.description) { @@ -210,7 +210,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } if (tool === "batch") { return `${parameters.tool_calls?.length || 0} parallel tools` } - + // ===== Fallback ===== // For unknown tools, custom tools, or tools without extractable keys // Check if parameters is empty or only has empty values diff --git a/lib/janitor.ts b/lib/janitor.ts index 5895adf..3d50aa1 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -228,7 +228,7 @@ export class Janitor { // Filter LLM results to only include IDs that were actually candidates // (LLM sometimes returns duplicate IDs that were already filtered out) const rawLlmPrunedIds = result.object.pruned_tool_call_ids - llmPrunedIds = rawLlmPrunedIds.filter(id => + llmPrunedIds = rawLlmPrunedIds.filter(id => prunableToolCallIds.includes(id.toLowerCase()) ) @@ -263,7 +263,7 @@ export class Janitor { // Calculate which IDs are actually NEW (not already pruned) const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id)) - + // finalPrunedIds includes everything (new + already pruned) for logging const finalPrunedIds = Array.from(expandedPrunedIds) @@ -338,16 +338,16 @@ export class Janitor { const shortenedPath = this.shortenSinglePath(pathPart) return `${prefix} in ${shortenedPath}` } - + return this.shortenSinglePath(input) } - + /** * Shorten a single path string */ private shortenSinglePath(path: string): string { const homeDir = require('os').homedir() - + // Strip working directory FIRST (before ~ replacement) for cleaner relative paths if (this.workingDirectory) { if (path.startsWith(this.workingDirectory + '/')) { @@ -358,7 +358,7 @@ export class Janitor { return '.' } } - + // Replace home directory with ~ if (path.startsWith(homeDir)) { path = '~' + path.slice(homeDir.length) @@ -375,7 +375,7 @@ export class Janitor { const workingDirWithTilde = this.workingDirectory.startsWith(homeDir) ? '~' + this.workingDirectory.slice(homeDir.length) : null - + if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) { return path.slice(workingDirWithTilde.length + 1) } @@ -396,15 +396,15 @@ export class Janitor { if (prunedIds.length === 0) return messages const prunedIdsSet = new Set(prunedIds.map(id => id.toLowerCase())) - + return messages.map(msg => { if (!msg.parts) return msg - + return { ...msg, parts: msg.parts.map((part: any) => { - if (part.type === 'tool' && - part.callID && + if (part.type === 'tool' && + part.callID && prunedIdsSet.has(part.callID.toLowerCase()) && part.state?.output) { // Replace with the same placeholder used by the global fetch wrapper @@ -427,20 +427,20 @@ export class Janitor { */ private async calculateTokensSaved(prunedIds: string[], toolOutputs: Map): Promise { const outputsToTokenize: string[] = [] - + for (const prunedId of prunedIds) { const output = toolOutputs.get(prunedId) if (output) { outputsToTokenize.push(output) } } - + if (outputsToTokenize.length > 0) { // Use batch tokenization for efficiency (lazy loads gpt-tokenizer) const tokenCounts = await estimateTokensBatch(outputsToTokenize) return tokenCounts.reduce((sum, count) => sum + count, 0) } - + return 0 } @@ -501,7 +501,7 @@ export class Janitor { const toolText = totalPruned === 1 ? 'tool' : 'tools' let message = `๐Ÿงน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)` - + // Add session totals if there's been more than one pruning run if (sessionStats.totalToolsPruned > totalPruned) { message += ` โ”‚ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools` @@ -536,7 +536,7 @@ export class Janitor { const toolText = deduplicatedIds.length === 1 ? 'tool' : 'tools' let message = `๐Ÿงน DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)` - + // Add session totals if there's been more than one pruning run if (sessionStats.totalToolsPruned > deduplicatedIds.length) { message += ` โ”‚ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools` @@ -544,7 +544,7 @@ export class Janitor { message += '\n' // Group by tool type - const grouped = new Map>() + const grouped = new Map>() for (const [_, details] of deduplicationDetails) { const { toolName, parameterKey, duplicateCount } = details @@ -603,7 +603,7 @@ export class Janitor { const tokensFormatted = formatTokenCount(tokensSaved) let message = `๐Ÿงน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)` - + // Add session totals if there's been more than one pruning run if (sessionStats.totalToolsPruned > totalPruned) { message += ` โ”‚ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools` @@ -615,7 +615,7 @@ export class Janitor { message += `\n๐Ÿ“ฆ Duplicates removed (${deduplicatedIds.length}):\n` // Group by tool type - const grouped = new Map>() + const grouped = new Map>() for (const [_, details] of deduplicationDetails) { const { toolName, parameterKey, duplicateCount } = details @@ -652,7 +652,7 @@ export class Janitor { } } } - + // Handle any tools that weren't found in metadata (edge case) const foundToolNames = new Set(toolsSummary.keys()) const missingTools = llmPrunedIds.filter(id => { @@ -660,7 +660,7 @@ export class Janitor { const metadata = toolMetadata.get(normalizedId) return !metadata || !foundToolNames.has(metadata.tool) }) - + if (missingTools.length > 0) { message += ` (${missingTools.length} tool${missingTools.length > 1 ? 's' : ''} with unknown metadata)\n` } diff --git a/lib/version-checker.ts b/lib/version-checker.ts new file mode 100644 index 0000000..fcc0c97 --- /dev/null +++ b/lib/version-checker.ts @@ -0,0 +1,88 @@ +// version-checker.ts - Checks for DCP updates on npm and shows toast notification +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +export const PACKAGE_NAME = '@tarquinen/opencode-dcp' +export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest` + +// ESM-compatible __dirname +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +/** + * Gets the local package version from package.json + */ +export function getLocalVersion(): string { + try { + const pkgPath = join(__dirname, '../package.json') + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) + return pkg.version + } catch { + return '0.0.0' + } +} + +/** + * Fetches the latest version from npm registry + */ +export async function getNpmVersion(): Promise { + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 5000) // 5s timeout + + const res = await fetch(NPM_REGISTRY_URL, { + signal: controller.signal, + headers: { 'Accept': 'application/json' } + }) + clearTimeout(timeout) + + if (!res.ok) return null + const data = await res.json() as { version?: string } + return data.version ?? null + } catch { + return null + } +} + +/** + * Compares semver versions. Returns true if remote > local + */ +export function isOutdated(local: string, remote: string): boolean { + const parseVersion = (v: string) => v.split('.').map(n => parseInt(n, 10) || 0) + const [localParts, remoteParts] = [parseVersion(local), parseVersion(remote)] + + for (let i = 0; i < Math.max(localParts.length, remoteParts.length); i++) { + const l = localParts[i] ?? 0 + const r = remoteParts[i] ?? 0 + if (r > l) return true + if (l > r) return false + } + return false +} + +/** + * Checks for updates and shows a toast if outdated. + * Fire-and-forget: does not throw, logs errors silently. + */ +export async function checkForUpdates(client: any): Promise { + try { + const local = getLocalVersion() + const npm = await getNpmVersion() + + if (!npm || !isOutdated(local, npm)) { + return // Up to date or couldn't fetch + } + + await client.tui.showToast({ + body: { + title: "DCP: Update available", + message: `v${local} โ†’ v${npm}\nUpdate opencode.jsonc: ${PACKAGE_NAME}@${npm}`, + variant: "info", + duration: 6000 + } + }) + } catch { + // Silently fail - version check is non-critical + } +} diff --git a/package-lock.json b/package-lock.json index c3032a9..b2abf5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.11", + "version": "0.3.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.11", + "version": "0.3.12", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", @@ -20,6 +20,7 @@ "devDependencies": { "@opencode-ai/plugin": ">=0.13.7", "@types/node": "^24.10.1", + "tsx": "^4.20.6", "typescript": "^5.9.3" }, "peerDependencies": { @@ -787,6 +788,448 @@ "node": ">=18.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@openauthjs/openauth": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@openauthjs/openauth/-/openauth-0.4.3.tgz", @@ -1551,6 +1994,48 @@ "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -1578,6 +2063,34 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gpt-tokenizer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", @@ -1649,6 +2162,16 @@ "type-fest": "^4.41.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/strnum": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", @@ -1667,6 +2190,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", diff --git a/package.json b/package.json index 9f2c382..47bfe92 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "0.3.11", + "version": "0.3.12", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js", @@ -12,7 +12,8 @@ "postbuild": "rm -rf dist/logs", "prepublishOnly": "npm run build", "dev": "opencode plugin dev", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test tests/*.test.ts" }, "keywords": [ "opencode", @@ -48,6 +49,7 @@ "devDependencies": { "@opencode-ai/plugin": ">=0.13.7", "@types/node": "^24.10.1", + "tsx": "^4.20.6", "typescript": "^5.9.3" }, "files": [