diff --git a/src/core/agent-debug-log.test.ts b/src/core/agent-debug-log.test.ts index 842de94..ceb547f 100644 --- a/src/core/agent-debug-log.test.ts +++ b/src/core/agent-debug-log.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, unlinkSync, existsSync } from 'node:fs'; +import { mkdtempSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { @@ -9,6 +9,9 @@ import { readAgentDebugLog, parseAgentDebugNdjson, extractNdjsonFromLogCapture, + discoverAgentDebugLogPaths, + listAgentDebugLogResources, + agentDebugLogResourceUri, } from './agent-debug-log.js'; describe('agent-debug-log', () => { @@ -91,4 +94,34 @@ describe('agent-debug-log', () => { expect(result.exists).toBe(false); expect(result.entries).toEqual([]); }); + + it('discoverAgentDebugLogPaths finds debug-*.log under .cursor', () => { + const cursorDir = join(tempDir, '.cursor'); + mkdirSync(cursorDir, { recursive: true }); + const debugLog = join(cursorDir, 'debug-abc123.log'); + writeFileSync(debugLog, '{"message":"x"}\n'); + writeFileSync(join(cursorDir, 'settings.json'), '{}'); + + expect(discoverAgentDebugLogPaths([tempDir])).toEqual([debugLog]); + expect(listAgentDebugLogResources([tempDir])).toEqual([ + { + uri: agentDebugLogResourceUri(debugLog), + name: 'Agent debug NDJSON log', + description: `NDJSON debug log at ${debugLog}`, + mimeType: 'application/json', + }, + ]); + }); + + it('listAgentDebugLogResources returns placeholder when no logs exist', () => { + expect(listAgentDebugLogResources([tempDir])).toEqual([ + { + uri: 'xcodebazel://agent-debug-log', + name: 'Agent debug NDJSON log', + description: + 'Structured agent debug log. Use bazel_ios_agent_debug_log_read or pass ?path= on the URI.', + mimeType: 'application/json', + }, + ]); + }); }); diff --git a/src/core/agent-debug-log.ts b/src/core/agent-debug-log.ts index cb656cd..4240770 100644 --- a/src/core/agent-debug-log.ts +++ b/src/core/agent-debug-log.ts @@ -1,5 +1,5 @@ -import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, copyFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, copyFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; import { runCommand } from '../utils/process.js'; export const AGENT_DEBUG_ENV_LOG_PATH = 'AGENT_DEBUG_LOG_PATH'; @@ -142,6 +142,70 @@ export function readAgentDebugLog(options: AgentDebugReadOptions): AgentDebugRea }; } +export function agentDebugLogResourceUri(logPath: string): string { + return `xcodebazel://agent-debug-log?path=${encodeURIComponent(logPath)}`; +} + +export interface AgentDebugLogResource { + uri: string; + name: string; + description: string; + mimeType: string; +} + +export function discoverAgentDebugLogPaths(searchRoots: string[]): string[] { + const seen = new Set(); + const discovered: Array<{ path: string; mtimeMs: number }> = []; + + for (const root of searchRoots) { + const cursorDir = join(resolve(root), '.cursor'); + if (!existsSync(cursorDir)) continue; + for (const entry of readdirSync(cursorDir)) { + if (!entry.startsWith('debug-') || !entry.endsWith('.log')) continue; + const logPath = resolve(cursorDir, entry); + if (seen.has(logPath)) continue; + seen.add(logPath); + discovered.push({ path: logPath, mtimeMs: statSync(logPath).mtimeMs }); + } + } + + return discovered.sort((a, b) => b.mtimeMs - a.mtimeMs).map((entry) => entry.path); +} + +export function listAgentDebugLogResources(searchRoots: string[]): AgentDebugLogResource[] { + const logPaths = discoverAgentDebugLogPaths(searchRoots); + if (logPaths.length === 0) { + return [ + { + uri: 'xcodebazel://agent-debug-log', + name: 'Agent debug NDJSON log', + description: + 'Structured agent debug log. Use bazel_ios_agent_debug_log_read or pass ?path= on the URI.', + mimeType: 'application/json', + }, + ]; + } + + return logPaths.map((logPath, index) => ({ + uri: agentDebugLogResourceUri(logPath), + name: index === 0 ? 'Agent debug NDJSON log' : `Agent debug log (${logPath.split('/').pop()})`, + description: `NDJSON debug log at ${logPath}`, + mimeType: 'application/json', + })); +} + +export function readAgentDebugLogResourceHelp(searchRoots: string[]): Record { + const discoveredPaths = discoverAgentDebugLogPaths(searchRoots); + return { + kind: 'agent-debug-log-help', + message: + 'Provide ?path= on the URI, or use bazel_ios_agent_debug_log_read / bazel_ios_agent_debug_log_pull.', + template: 'xcodebazel://agent-debug-log?path={path}', + discoveredPaths, + suggestedUris: discoveredPaths.map(agentDebugLogResourceUri), + }; +} + export function parseAgentDebugLogUri(uri: string): string | null { try { const parsed = new URL(uri); diff --git a/src/mcp/server.test.ts b/src/mcp/server.test.ts index 6a56c64..31f0873 100644 --- a/src/mcp/server.test.ts +++ b/src/mcp/server.test.ts @@ -109,11 +109,23 @@ describe('MCP server protocol', () => { ]); const resp = lines.map((l) => JSON.parse(l)).find((r) => r.id === 2); - expect(resp.result.resources).toHaveLength(2); + expect(resp.result.resources.length).toBeGreaterThanOrEqual(3); const uris = resp.result.resources.map((r: { uri: string }) => r.uri); expect(uris).toContain('xcodebazel://last-command'); expect(uris).toContain('xcodebazel://session-status'); - expect(uris).not.toContain('xcodebazel://agent-debug-log'); + expect(uris.some((uri: string) => uri.startsWith('xcodebazel://agent-debug-log'))).toBe(true); + }); + + it('reads bare agent-debug-log resource without error', async () => { + const lines = await sendMcpRequest([ + { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } }, + { jsonrpc: '2.0', id: 2, method: 'resources/read', params: { uri: 'xcodebazel://agent-debug-log' } }, + ]); + + const resp = lines.map((l) => JSON.parse(l)).find((r) => r.id === 2); + expect(resp.error).toBeUndefined(); + const parsed = JSON.parse(resp.result.contents[0].text); + expect(parsed.kind).toBe('agent-debug-log-help'); }); it('lists agent-debug-log as a resource template (requires path param)', async () => { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 09bf16b..2fd4a49 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -75,6 +75,13 @@ async function handleMessage(message: JsonRpcMessage): Promise { } if (method === 'prompts/list') return sendResult(id, { prompts: [] }); if (method === 'resources/list') { + const { listAgentDebugLogResources } = await import('../core/agent-debug-log.js'); + const { getConfig: getRuntimeConfig } = await import('../runtime/config.js'); + const runtimeConfig = getRuntimeConfig(); + const agentDebugResources = listAgentDebugLogResources([ + runtimeConfig.workspacePath, + process.cwd(), + ]); return sendResult(id, { resources: [ { @@ -89,6 +96,7 @@ async function handleMessage(message: JsonRpcMessage): Promise { description: 'Current session state: active workflows, defaults, uptime.', mimeType: 'application/json', }, + ...agentDebugResources, ], }); } @@ -150,10 +158,21 @@ async function handleMessage(message: JsonRpcMessage): Promise { }); } if (params?.uri?.startsWith('xcodebazel://agent-debug-log')) { - const { parseAgentDebugLogUri, readAgentDebugLog } = await import('../core/agent-debug-log.js'); + const { parseAgentDebugLogUri, readAgentDebugLog, readAgentDebugLogResourceHelp } = await import('../core/agent-debug-log.js'); + const { getConfig: getRuntimeConfig } = await import('../runtime/config.js'); + const runtimeConfig = getRuntimeConfig(); const logPath = parseAgentDebugLogUri(params.uri); if (!logPath) { - throw new Error('agent-debug-log resource requires ?path= query parameter.'); + const help = readAgentDebugLogResourceHelp([runtimeConfig.workspacePath, process.cwd()]); + return sendResult(id, { + contents: [ + { + uri: params.uri, + mimeType: 'application/json', + text: JSON.stringify(help, null, 2), + }, + ], + }); } const result = readAgentDebugLog({ logPath }); return sendResult(id, {