diff --git a/index.ts b/index.ts index 191623c..e8585c9 100644 --- a/index.ts +++ b/index.ts @@ -56,7 +56,7 @@ const plugin: Plugin = (async (ctx) => { const stateManager = new StateManager() const toolParametersCache = new Map() // callID -> parameters const modelCache = new Map() // sessionID -> model info - const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary) + const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary, ctx.directory) const cacheToolParameters = (messages: any[], component: string) => { for (const message of messages) { diff --git a/lib/deduplicator.ts b/lib/deduplicator.ts index d381acd..7fc7a74 100644 --- a/lib/deduplicator.ts +++ b/lib/deduplicator.ts @@ -151,16 +151,24 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } } // ===== Directory/Search Tools ===== - if (tool === "list" && parameters.path) { - return parameters.path - } - if (tool === "glob" && parameters.pattern) { - const pathInfo = parameters.path ? ` in ${parameters.path}` : "" - return `"${parameters.pattern}"${pathInfo}` + if (tool === "list") { + // path is optional, defaults to current directory + return parameters.path || '(current directory)' + } + if (tool === "glob") { + // pattern is required for glob + if (parameters.pattern) { + const pathInfo = parameters.path ? ` in ${parameters.path}` : "" + return `"${parameters.pattern}"${pathInfo}` + } + return '(unknown pattern)' } - if (tool === "grep" && parameters.pattern) { - const pathInfo = parameters.path ? ` in ${parameters.path}` : "" - return `"${parameters.pattern}"${pathInfo}` + if (tool === "grep") { + if (parameters.pattern) { + const pathInfo = parameters.path ? ` in ${parameters.path}` : "" + return `"${parameters.pattern}"${pathInfo}` + } + return '(unknown pattern)' } // ===== Execution Tools ===== @@ -205,5 +213,10 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any } // ===== Fallback ===== // For unknown tools, custom tools, or tools without extractable keys - return JSON.stringify(parameters).substring(0, 50) + // Check if parameters is empty or only has empty values + const paramStr = JSON.stringify(parameters) + if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') { + return '' // Return empty to trigger (default) fallback in UI + } + return paramStr.substring(0, 50) } diff --git a/lib/janitor.ts b/lib/janitor.ts index b5ad60e..f41f04f 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -17,7 +17,8 @@ export class Janitor { private configModel?: string, // Format: "provider/model" private showModelErrorToasts: boolean = true, // Whether to show toast for model errors private pruningMode: "auto" | "smart" = "smart", // Pruning strategy - private pruningSummary: "off" | "minimal" | "detailed" = "detailed" // UI summary display mode + private pruningSummary: "off" | "minimal" | "detailed" = "detailed", // UI summary display mode + private workingDirectory?: string // Current working directory for relative path display ) { } /** @@ -490,6 +491,23 @@ export class Janitor { return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}` } + // Strip working directory to show relative paths + if (this.workingDirectory) { + // Try to match against the absolute working directory first + if (path.startsWith(this.workingDirectory + '/')) { + return path.slice(this.workingDirectory.length + 1) + } + + // Also try matching against ~ version of working directory + const workingDirWithTilde = this.workingDirectory.startsWith(homeDir) + ? '~' + this.workingDirectory.slice(homeDir.length) + : null + + if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) { + return path.slice(workingDirWithTilde.length + 1) + } + } + return path } @@ -553,6 +571,8 @@ export class Janitor { /** * Build a summary of tools by grouping them * Uses shared extractParameterKey logic for consistent parameter extraction + * + * Note: prunedIds may be in original case (from LLM) but toolMetadata uses lowercase keys */ private buildToolsSummary(prunedIds: string[], toolMetadata: Map): Map { const toolsSummary = new Map() @@ -564,7 +584,9 @@ export class Janitor { } for (const prunedId of prunedIds) { - const metadata = toolMetadata.get(prunedId) + // Normalize ID to lowercase for lookup (toolMetadata uses lowercase keys) + const normalizedId = prunedId.toLowerCase() + const metadata = toolMetadata.get(normalizedId) if (metadata) { const toolName = metadata.tool if (!toolsSummary.has(toolName)) { @@ -577,6 +599,10 @@ export class Janitor { // Apply path shortening and truncation for display const displayKey = truncate(this.shortenPath(paramKey), 80) toolsSummary.get(toolName)!.push(displayKey) + } else { + // For tools with no extractable parameter key, add a placeholder + // This ensures the tool still shows up in the summary + toolsSummary.get(toolName)!.push('(default)') } } } @@ -736,17 +762,20 @@ export class Janitor { for (const param of params) { message += ` ${param}\n` } - } else { - // For tools with no specific params (like batch), just show the tool name and count - const count = llmPrunedIds.filter(id => { - const m = toolMetadata.get(id) - return m && m.tool === toolName - }).length - if (count > 0) { - message += ` ${toolName} (${count})\n` - } } } + + // Handle any tools that weren't found in metadata (edge case) + const foundToolNames = new Set(toolsSummary.keys()) + const missingTools = llmPrunedIds.filter(id => { + const normalizedId = id.toLowerCase() + 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` + } } await this.sendIgnoredMessage(sessionID, message.trim()) diff --git a/lib/model-selector.ts b/lib/model-selector.ts index d7baebe..70d4986 100644 --- a/lib/model-selector.ts +++ b/lib/model-selector.ts @@ -65,6 +65,42 @@ function shouldSkipProvider(providerID: string): boolean { return SKIP_PROVIDERS.some(skip => normalized.includes(skip.toLowerCase())); } +/** + * Attempts to import OpencodeAI with retry logic to handle plugin initialization timing issues. + * Some providers (like openai via @openhax/codex) may not be fully initialized on first attempt. + */ +async function importOpencodeAI(logger?: Logger, maxRetries: number = 3, delayMs: number = 100): Promise { + let lastError: Error | undefined; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const { OpencodeAI } = await import('@tarquinen/opencode-auth-provider'); + return new OpencodeAI(); + } catch (error: any) { + lastError = error; + + // Check if this is the specific initialization error we're handling + if (error.message?.includes('before initialization')) { + logger?.debug('model-selector', `Import attempt ${attempt}/${maxRetries} failed, will retry`, { + error: error.message + }); + + if (attempt < maxRetries) { + // Wait before retrying, with exponential backoff + await new Promise(resolve => setTimeout(resolve, delayMs * attempt)); + continue; + } + } + + // For other errors, don't retry + throw error; + } + } + + // All retries exhausted + throw lastError; +} + /** * Main model selection function with intelligent fallback logic * @@ -85,9 +121,9 @@ export async function selectModel( ): Promise { logger?.info('model-selector', 'Model selection started', { currentModel, configModel }); - // Lazy import - only load the 812KB auth provider package when actually needed - const { OpencodeAI } = await import('@tarquinen/opencode-auth-provider'); - const opencodeAI = new OpencodeAI(); + // Lazy import with retry logic - handles plugin initialization timing issues + // Some providers (like openai via @openhax/codex) may not be ready on first attempt + const opencodeAI = await importOpencodeAI(logger); let failedModelInfo: ModelInfo | undefined; @@ -178,10 +214,10 @@ export async function selectModel( logger?.info('model-selector', 'Available authenticated providers', { providerCount: availableProviderIDs.length, providerIDs: availableProviderIDs, - providers: Object.entries(providers).map(([id, info]) => ({ + providers: Object.entries(providers).map(([id, info]: [string, any]) => ({ id, source: info.source, - name: info.info.name + name: info.info?.name })) }); diff --git a/package-lock.json b/package-lock.json index ef31df6..aa494e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.5", + "version": "0.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index c6273d4..939957b 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.5", + "version": "0.3.6", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",