diff --git a/src/agents/plugins/claude/plugin/__tests__/statusline.test.ts b/src/agents/plugins/claude/plugin/__tests__/statusline.test.ts index 55ec99be8..c3e083b19 100644 --- a/src/agents/plugins/claude/plugin/__tests__/statusline.test.ts +++ b/src/agents/plugins/claude/plugin/__tests__/statusline.test.ts @@ -222,7 +222,7 @@ describe('resolveBudget', () => { })); const getAuthHeadersImpl = vi.fn().mockResolvedValue(null); const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl }); - expect(result).toEqual({ budget: null, budgetError: 'reauthenticate' }); + expect(result).toEqual({ budget: null, budgetError: 'reauthenticate - run codemie profile login' }); }); it('returns the HTTP error message when the fetch fails', async () => { diff --git a/src/agents/plugins/claude/plugin/statusline.mjs b/src/agents/plugins/claude/plugin/statusline.mjs index f2f900167..5c76b5c44 100644 --- a/src/agents/plugins/claude/plugin/statusline.mjs +++ b/src/agents/plugins/claude/plugin/statusline.mjs @@ -27,16 +27,27 @@ const ENCRYPTION_KEY = (() => { function decrypt(text) { const parts = text.split(':'); if (parts.length === 3) { - const iv = Buffer.from(parts[0], 'hex'); - const authTag = Buffer.from(parts[1], 'hex'); - const d = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv); - d.setAuthTag(authTag); - return d.update(parts[2], 'hex', 'utf8') + d.final('utf8'); + // Current GCM format: iv:authTag:encrypted + try { + const iv = Buffer.from(parts[0], 'hex'); + const authTag = Buffer.from(parts[1], 'hex'); + const d = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv); + d.setAuthTag(authTag); + return d.update(parts[2], 'hex', 'utf8') + d.final('utf8'); + } catch (gcmError) { + // If GCM fails, try CBC as fallback for malformed data + console.error(`[CodeMie Statusline] GCM decryption failed, trying CBC fallback: ${gcmError.message}`); + } } // Legacy CBC format: iv:encrypted (backward compat for existing stored credentials) - const iv = Buffer.from(parts[0], 'hex'); - const d = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); - return d.update(parts[1], 'hex', 'utf8') + d.final('utf8'); + try { + const iv = Buffer.from(parts[0], 'hex'); + const d = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); + return d.update(parts[1], 'hex', 'utf8') + d.final('utf8'); + } catch (cbcError) { + console.error(`[CodeMie Statusline] CBC decryption failed: ${cbcError.message}`); + throw new Error('Failed to decrypt credentials - please re-authenticate with codemie profile login'); + } } function urlHash(rawUrl) { @@ -225,10 +236,15 @@ export async function resolveBudget({ try { headers = await getAuthHeadersImpl(codeMieUrl); } catch (e) { - return { budget: null, budgetError: e.message }; + const errorMsg = e.message || String(e); + // Provide more specific error messages instead of generic "reauthenticate" + if (errorMsg.includes('decrypt') || errorMsg.includes('cipher') || errorMsg.includes('authentication')) { + return { budget: null, budgetError: 'auth error - try codemie profile login' }; + } + return { budget: null, budgetError: errorMsg }; } if (!headers) { - return { budget: null, budgetError: 'reauthenticate' }; + return { budget: null, budgetError: 'reauthenticate - run codemie profile login' }; } try { diff --git a/src/agents/plugins/claude/statusline-installer.ts b/src/agents/plugins/claude/statusline-installer.ts index df979fd9d..ef7a5d8ca 100644 --- a/src/agents/plugins/claude/statusline-installer.ts +++ b/src/agents/plugins/claude/statusline-installer.ts @@ -53,7 +53,23 @@ export async function installStatusline(): Promise { } } - const alreadyConfigured = Boolean(settings.statusLine); + // Check if there's an existing CodeMie statusline configuration that needs migration + const existingStatusLine = settings.statusLine as Record | undefined; + let alreadyConfigured = false; + + if (existingStatusLine) { + // Check if the existing statusLine points to a CodeMie script (old or new) + const command = String(existingStatusLine.command || ''); + if (command.includes('codemie-budget-status.js') || command.includes('codemie-statusline.mjs')) { + // This is a CodeMie-managed statusline - migrate it to the current script + logger.info('[Statusline] Migrating existing CodeMie statusline configuration to current script'); + alreadyConfigured = true; + } else { + // This is a non-CodeMie statusline - don't touch it + logger.debug('[Statusline] Existing statusLine is not CodeMie-managed, leaving unchanged'); + return { scriptPath, alreadyConfigured: true }; + } + } settings.statusLine = { type: 'command', diff --git a/src/migrations/__tests__/migration-runner-ordering.test.ts b/src/migrations/__tests__/migration-runner-ordering.test.ts index 2c8b3af4c..9176bd1dd 100644 --- a/src/migrations/__tests__/migration-runner-ordering.test.ts +++ b/src/migrations/__tests__/migration-runner-ordering.test.ts @@ -85,7 +85,14 @@ afterEach(async () => { openLoggers = []; if (originalCodemieHome !== undefined) process.env.CODEMIE_HOME = originalCodemieHome; else delete process.env.CODEMIE_HOME; - rmSync(tmpHome, { recursive: true, force: true }); + // Windows can still hold log file handles briefly after stream.end(); force + + // maxRetries only suppress ENOENT, so treat residual ENOTEMPTY/EBUSY as + // best-effort teardown (matches claude.metrics-processor-names.test.ts). + try { + rmSync(tmpHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } catch { + /* ignore temp-dir cleanup races */ + } vi.restoreAllMocks(); });