From f0c744f467e0128a056d92704f5e05cabe19946a Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:32:33 +0800 Subject: [PATCH 1/9] feat: Add customer_logs event handler to WebSocket client --- src/lib/api/websocket.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/api/websocket.ts b/src/lib/api/websocket.ts index a1f1c30..e957b74 100644 --- a/src/lib/api/websocket.ts +++ b/src/lib/api/websocket.ts @@ -75,6 +75,9 @@ export class WebSocketClient extends EventEmitter { case 'engineer_connected': this.emit('engineer_connected'); break; + case 'customer_console_logs': + this.emit('customer_console_logs', message.payload); + break; default: this.emit('message', message); } From 7dd7544593fcafb22dcb653bc65acb953356e423 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:32:58 +0800 Subject: [PATCH 2/9] feat: Create watch-logs CLI command for real-time log file streaming via WebSocket --- src/commands/watch-logs.ts | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/commands/watch-logs.ts diff --git a/src/commands/watch-logs.ts b/src/commands/watch-logs.ts new file mode 100644 index 0000000..d72fe49 --- /dev/null +++ b/src/commands/watch-logs.ts @@ -0,0 +1,117 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { AuthManager } from '../modules/auth.js'; +import { ApiClient } from '../modules/api.js'; +import { handleError } from '../utils/errors.js'; +import { WebSocketClient } from '../lib/api/websocket.js'; + +interface FileWatchState { + lastPosition: number; + taskId: string; +} + +export async function watchLogsCommand(taskId?: string): Promise { + const authManager = new AuthManager(); + + if (!authManager.isAuthenticated()) { + console.log('Error: Not authenticated. Please run: codevf login'); + process.exit(1); + } + + if (!taskId) { + console.log('Error: Task ID is required'); + console.log('Usage: codevf watch-logs '); + process.exit(1); + } + + const logsPath = path.join(process.cwd(), 'logs.txt'); + const apiClient = new ApiClient(authManager); + let wsClient: WebSocketClient | null = null; + + // Connect to WebSocket + try { + wsClient = new WebSocketClient(authManager); + await wsClient.connect(taskId); + } catch (error) { + console.error('Error connecting to WebSocket:', error); + process.exit(1); + } + + // Check if logs.txt exists + if (!fs.existsSync(logsPath)) { + console.log(`logs.txt not found at ${logsPath}`); + console.log('Waiting for logs.txt to be created...'); + } + + const watchState: FileWatchState = { + lastPosition: 0, + taskId, + }; + + // Initial read if file exists + if (fs.existsSync(logsPath)) { + const content = fs.readFileSync(logsPath, 'utf-8'); + watchState.lastPosition = Buffer.byteLength(content, 'utf-8'); + } + + console.log(`Watching logs.txt for task ${taskId}`); + console.log('Press Ctrl+C to stop watching'); + + // Watch for file changes + const watcher = fs.watch(logsPath, async (eventType, filename) => { + if (eventType === 'change' && filename === 'logs.txt') { + try { + await sendNewLogs(watchState, wsClient); + } catch (error) { + console.error('Error sending logs:', error); + } + } + }); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('Stopping log watcher'); + watcher.close(); + if (wsClient) { + wsClient.disconnect(); + } + process.exit(0); + }); +} + +async function sendNewLogs( + state: FileWatchState, + wsClient: WebSocketClient | null +): Promise { + const logsPath = path.join(process.cwd(), 'logs.txt'); + + if (!fs.existsSync(logsPath) || !wsClient) { + return; + } + + try { + const content = fs.readFileSync(logsPath, 'utf-8'); + const contentBytes = Buffer.byteLength(content, 'utf-8'); + + // Only send if there's new content + if (contentBytes > state.lastPosition) { + const newContent = content.slice(state.lastPosition); + state.lastPosition = contentBytes; + + // Send via WebSocket + wsClient.send({ + payload: newContent, + type: 'customer_console_logs' + }); + + console.log( + `Sent ${newContent.length} bytes of new logs to engineer` + ); + } + } catch (error) { + // Silently ignore file read errors (file might be in use) + if ((error as any).code !== 'ENOENT') { + handleError(error); + } + } +} From fe46018a491e2e3284fc5246cd603442e1a434b0 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:33:04 +0800 Subject: [PATCH 3/9] feat: Register watch-logs command in CLI --- src/index.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/index.ts b/src/index.ts index d0dde20..4b894c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { fixCommand } from './commands/fix.js'; import { tasksCommand } from './commands/tasks.js'; import { chatCommand } from './commands/chat.js'; import { listenCommand } from './commands/listen.js'; +import { watchLogsCommand } from './commands/watch-logs.js'; import { startMcpHttp, startMcpStdio } from './commands/mcp.js'; import { handleError } from './utils/errors.js'; import { ConfigManager } from './modules/config.js'; @@ -344,6 +345,24 @@ if (args.length === 0) { } } ) + .command( + 'watch-logs ', + 'Watch logs.txt and stream updates to an active chat session', + (yargs) => { + return yargs.positional('task-id', { + type: 'string', + describe: 'Task ID to send log updates to', + demandOption: true, + }); + }, + async (argv) => { + try { + await watchLogsCommand(argv['task-id'] as string); + } catch (error) { + handleError(error); + } + } + ) .command( 'fix ', 'Start a live debugging session', From 9ab6e691e5d7b6fcc746666d58f0529592d8c2be Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:33:13 +0800 Subject: [PATCH 4/9] refactor: Update setup command for consistency --- src/commands/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 3148af2..68429cc 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -10,8 +10,8 @@ import path from 'path'; import os from 'os'; import { ConfigManager } from '../lib/config/manager.js'; import { OAuthFlow } from '../lib/auth/oauth-flow.js'; -import { commandContent } from './cvf-command-content.js'; -import { chatCommandContent } from './cvf-chat-command-content.js'; +import { commandContent } from './contents/cvf-command-content.js'; +import { chatCommandContent } from './contents/cvf-chat-command-content.js'; /** * Get Claude Code config path based on platform From 76d200afe387b28e9aa0a00a5826ac92e874a009 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:33:17 +0800 Subject: [PATCH 5/9] feat: Update Claude instructions with log streaming and command execution guidance --- .../contents/cvf-chat-command-content.ts | 66 ++++++++++ src/commands/contents/cvf-command-content.ts | 122 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/commands/contents/cvf-chat-command-content.ts create mode 100644 src/commands/contents/cvf-command-content.ts diff --git a/src/commands/contents/cvf-chat-command-content.ts b/src/commands/contents/cvf-chat-command-content.ts new file mode 100644 index 0000000..e44a7b9 --- /dev/null +++ b/src/commands/contents/cvf-chat-command-content.ts @@ -0,0 +1,66 @@ +export const chatCommandContent = `--- +description: Start an extended CodeVF engineer chat session with full context +--- + +# CodeVF Engineer Chat + +Please start an extended debugging session with a CodeVF engineer using the \`codevf-chat\` MCP tool. + +**Current context (include relevant logs, errors, and recent changes):** +{{PROMPT}} + +--- + +**Instructions for Claude:** + +1. Ask a short clarifying question if needed: "What's the issue?" +2. Call \`codevf-chat\` with: + - \`message\`: include the user's issue and any gathered context + - \`maxCredits\`: suggest 240 (or adjust if user specifies) +3. **IMPORTANT: Every time you successfully execute a command, run a task, or complete an action**, immediately call \`codevf-chat\` again with a JSON thought process in this format: + \`\`\`json + { + "type": "thought_process", + "action": "brief description of what was done", + "status": "success|in_progress|error", + "result": "what happened and outcome", + "next_step": "what to do next" + } + \`\`\` + Include this JSON in your message to the engineer. Example: "Executed command \\\`npm install\\\`. \`\`\`json {\"type\": \"thought_process\", \"action\": \"install dependencies\", \"status\": \"success\", \"result\": \"All packages installed\", \"next_step\": \"Run the dev server\"} \`\`\`" + +4. **When running commands that produce output**, redirect the output of the logs to \`logs.txt\` so the engineer can review results. For example: + - \`bun dev > logs.txt\` + - \`npm run dev > logs.txt\` + - \`npm test > logs.txt\` + - \`node script.js > logs.txt\` + + Always use the filename \`logs.txt\` for consistency. + +5. **IMPORTANT: Whenever the user requests the server to be up** (e.g., "start the dev server", "run the server", "get the app running", etc.), you MUST also start log streaming immediately after. Run one of these commands in a separate process: + - \`codevf watch-logs \` + - \`bun x codevf watch-logs \` (if codevf is not installed globally) + - \`npx codevf watch-logs \` (if needed) + + Replace \`\` with the actual task ID. This ensures the engineer sees all logs from the running server in real-time. Start this in a background process or separate terminal so it doesn't block the server from running. + +6. **When sending files to the engineer**, format your message as JSON in this structure: + \`\`\`json + { + "type": "file_message", + "message": "Brief description of what this file is or why you're sending it", + "file": { + "name": "filename.ext", + "path": "relative/path/to/file", + "content": "file content here (can be large)" + } + } + \`\`\` + The frontend will automatically detect this JSON structure and render the file as a badge with the ability to open the content in a dialog with markdown rendering. Example: \`\`\`json {\"type\": \"file_message\", \"message\": \"Here's the error log for debugging:\", \"file\": {\"name\": \"error.log\", \"path\": \"logs/error.log\", \"content\": \"[ERROR] Connection failed...\"}} \`\`\` +7. Return the engineer response or session link directly. + +**Notes:** +- The engineer interface will render thought process JSON blocks as formatted thought processes to help track progress +- File message JSON blocks will be detected and rendered as interactive file badges with dialog viewers +- Keep JSON formatting consistent - use valid JSON within markdown code blocks +`; diff --git a/src/commands/contents/cvf-command-content.ts b/src/commands/contents/cvf-command-content.ts new file mode 100644 index 0000000..6e53fe7 --- /dev/null +++ b/src/commands/contents/cvf-command-content.ts @@ -0,0 +1,122 @@ +export const commandContent = `--- +description: Ask a CodeVF engineer for help with code validation, debugging, or technical questions +--- + +# CodeVF Engineer Assistance + +Please help me with the following question or task by consulting a CodeVF engineer using the appropriate MCP tool: + +**My request:** +{{PROMPT}} + +--- + +**Available CodeVF Commands:** +- \`codevf setup\` - Configure MCP server for Claude Code +- \`codevf mcp stdio\` - Start MCP server over stdio +- \`codevf mcp http --port 3333\` - Start MCP server over HTTP/SSE +- \`codevf welcome\` - Show welcome/setup guide +- \`codevf login\` - Authenticate with CodeVF (disabled in beta) +- \`codevf logout\` - Clear local authentication (disabled in beta) +- \`codevf init\` - Initialize CodeVF in a project (disabled in beta) +- \`codevf sync\` - Sync local changes with CodeVF (disabled in beta) +- \`codevf fix \` - Start live debugging session (disabled in beta) +- \`codevf tasks\` - List open tasks (disabled in beta) +- \`codevf cvf-chat [project-id]\` - Join live chat session (disabled in beta) +- \`codevf cvf-listen\` - Monitor active chats (disabled in beta) + +**Instructions for Claude:** + +1. **Analyze the request** to determine which CodeVF tool is most appropriate: + - Use \`codevf-instant\` for: + - Quick validation questions (1-10 credits, ~2 min response) + - "Does this fix work?" + - "Is this approach correct?" + - "Can you identify the error?" + - UI/visual verification and feedback + - Simple technical questions + + - Use \`codevf-chat\` for: + - Complex debugging requiring back-and-forth (4-1920 credits, 2 credits/min) + - Multi-step troubleshooting + - Architecture discussions + - Extended collaboration + + - Use \`codevf-tunnel\` for: + - Creating secure tunnels to expose local dev servers + - Testing webhooks, OAuth callbacks, or external integrations + - Sharing local development environment with engineers + - No credits required - tunnel remains active for session + +2. **For UI/Visual Development Tasks** (Pain-Free Experience): + + When implementing UI changes or visual features, follow this iterative flow: + + **Step 1 - Initial Implementation:** + - Make the requested changes to code/styles + - Create tunnel if needed: \`codevf-tunnel\` with dev server port + - Share tunnel URL for visual verification + + **Step 2 - Engineer Verification:** + - Use \`codevf-instant\` with message like: + "Please verify this UI change via [tunnel-url] (password: [password]). Does it match the requested design/functionality?" + - **Always include both URL and password** - localtunnel requires password to bypass landing page + - Include specific areas of focus (layout, spacing, colors, interactions) + - Allow 5-8 credits for visual review and feedback + + **Step 3 - Iterative Refinement:** + - Apply engineer feedback immediately + - Use \`codevf-instant\` again for re-verification: + "Applied your feedback (reduced padding to 16px). Please check if this looks better now at [tunnel-url] (same password)." + - Continue this cycle until engineer confirms it looks right + - Each feedback cycle: 3-5 credits + + **Example UI Flow:** + \`\`\` + User: "Make the UI look like a modern card layout" + + Claude: + 1. Implements card styling + 2. Creates tunnel → https://abc123.loca.lt (password: xyz789) + 3. codevf-instant: "Please review the card layout at https://abc123.loca.lt + (password: xyz789). Does it look modern and clean? Any spacing/styling issues?" (5 credits) + + Engineer: "Cards look good but margins are too large, reduce to 16px" + + Claude: + 4. Updates margins to 16px + 5. codevf-instant: "Applied 16px margins. Please verify the spacing + looks better now at https://abc123.loca.lt (password: xyz789)." (3 credits) + + Engineer: "Perfect! The spacing looks much cleaner now." + \`\`\` + +3. **Use the appropriate tool:** + - For instant queries: Call \`codevf-instant\` with the message and appropriate maxCredits (1-10) + - For extended sessions: Call \`codevf-chat\` with the message and appropriate maxCredits (suggest 240 for ~2 hours) + - For tunnel access: Call \`codevf-tunnel\` with the port number (e.g., { "port": 3000 }) + +4. **Present the response:** + - For instant queries: Share the engineer's response directly + - For chat sessions: Provide the session URL so the user can monitor the conversation + - For tunnels: Share the public URL that was created + +**Credit Guidelines:** +- Instant validation: 1-10 credits (typically 3-5 credits per question) +- UI verification: 5-8 credits for initial review, 3-5 credits for follow-up checks +- Extended chat: 2 credits per minute (240 credits = 2 hours) +- Tunnel creation: Free (no credits required) + +**Planning Integration for Complex Tasks:** +When Claude is planning multi-step implementations, especially UI-heavy tasks: +- Always include "Engineer verification checkpoints" in the plan +- Budget credits for iterative feedback (typically 15-25 credits total for UI refinement) +- Create tunnel early in the process for continuous visual validation +- Plan for 2-4 feedback cycles to achieve the desired result + +**Example Usage:** +- \`/cvf Does this authentication fix prevent the timing attack?\` → Use codevf-instant +- \`/cvf Complex race condition in WebSocket reconnection needs debugging\` → Use codevf-chat +- \`/cvf Create tunnel to my dev server on port 3000\` → Use codevf-tunnel +- \`/cvf Make the login form look more modern and user-friendly\` → Use tunnel + iterative codevf-instant feedback +`; From 03a5db7791ffd91ca5d881f187e0bb8a116255fd Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 00:33:40 +0800 Subject: [PATCH 6/9] refactor: Remove obsolete chat and command content files --- src/commands/cvf-chat-command-content.ts | 21 ---- src/commands/cvf-command-content.ts | 122 ----------------------- 2 files changed, 143 deletions(-) delete mode 100644 src/commands/cvf-chat-command-content.ts delete mode 100644 src/commands/cvf-command-content.ts diff --git a/src/commands/cvf-chat-command-content.ts b/src/commands/cvf-chat-command-content.ts deleted file mode 100644 index f23724b..0000000 --- a/src/commands/cvf-chat-command-content.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const chatCommandContent = `--- -description: Start an extended CodeVF engineer chat session with full context ---- - -# CodeVF Engineer Chat - -Please start an extended debugging session with a CodeVF engineer using the \`codevf-chat\` MCP tool. - -**Current context (include relevant logs, errors, and recent changes):** -{{PROMPT}} - ---- - -**Instructions for Claude:** - -1. Ask a short clarifying question if needed: "What's the issue?" -2. Call \`codevf-chat\` with: - - \`message\`: include the user's issue and any gathered context - - \`maxCredits\`: suggest 240 (or adjust if user specifies) -3. Return the engineer response or session link directly. -`; diff --git a/src/commands/cvf-command-content.ts b/src/commands/cvf-command-content.ts deleted file mode 100644 index 6e53fe7..0000000 --- a/src/commands/cvf-command-content.ts +++ /dev/null @@ -1,122 +0,0 @@ -export const commandContent = `--- -description: Ask a CodeVF engineer for help with code validation, debugging, or technical questions ---- - -# CodeVF Engineer Assistance - -Please help me with the following question or task by consulting a CodeVF engineer using the appropriate MCP tool: - -**My request:** -{{PROMPT}} - ---- - -**Available CodeVF Commands:** -- \`codevf setup\` - Configure MCP server for Claude Code -- \`codevf mcp stdio\` - Start MCP server over stdio -- \`codevf mcp http --port 3333\` - Start MCP server over HTTP/SSE -- \`codevf welcome\` - Show welcome/setup guide -- \`codevf login\` - Authenticate with CodeVF (disabled in beta) -- \`codevf logout\` - Clear local authentication (disabled in beta) -- \`codevf init\` - Initialize CodeVF in a project (disabled in beta) -- \`codevf sync\` - Sync local changes with CodeVF (disabled in beta) -- \`codevf fix \` - Start live debugging session (disabled in beta) -- \`codevf tasks\` - List open tasks (disabled in beta) -- \`codevf cvf-chat [project-id]\` - Join live chat session (disabled in beta) -- \`codevf cvf-listen\` - Monitor active chats (disabled in beta) - -**Instructions for Claude:** - -1. **Analyze the request** to determine which CodeVF tool is most appropriate: - - Use \`codevf-instant\` for: - - Quick validation questions (1-10 credits, ~2 min response) - - "Does this fix work?" - - "Is this approach correct?" - - "Can you identify the error?" - - UI/visual verification and feedback - - Simple technical questions - - - Use \`codevf-chat\` for: - - Complex debugging requiring back-and-forth (4-1920 credits, 2 credits/min) - - Multi-step troubleshooting - - Architecture discussions - - Extended collaboration - - - Use \`codevf-tunnel\` for: - - Creating secure tunnels to expose local dev servers - - Testing webhooks, OAuth callbacks, or external integrations - - Sharing local development environment with engineers - - No credits required - tunnel remains active for session - -2. **For UI/Visual Development Tasks** (Pain-Free Experience): - - When implementing UI changes or visual features, follow this iterative flow: - - **Step 1 - Initial Implementation:** - - Make the requested changes to code/styles - - Create tunnel if needed: \`codevf-tunnel\` with dev server port - - Share tunnel URL for visual verification - - **Step 2 - Engineer Verification:** - - Use \`codevf-instant\` with message like: - "Please verify this UI change via [tunnel-url] (password: [password]). Does it match the requested design/functionality?" - - **Always include both URL and password** - localtunnel requires password to bypass landing page - - Include specific areas of focus (layout, spacing, colors, interactions) - - Allow 5-8 credits for visual review and feedback - - **Step 3 - Iterative Refinement:** - - Apply engineer feedback immediately - - Use \`codevf-instant\` again for re-verification: - "Applied your feedback (reduced padding to 16px). Please check if this looks better now at [tunnel-url] (same password)." - - Continue this cycle until engineer confirms it looks right - - Each feedback cycle: 3-5 credits - - **Example UI Flow:** - \`\`\` - User: "Make the UI look like a modern card layout" - - Claude: - 1. Implements card styling - 2. Creates tunnel → https://abc123.loca.lt (password: xyz789) - 3. codevf-instant: "Please review the card layout at https://abc123.loca.lt - (password: xyz789). Does it look modern and clean? Any spacing/styling issues?" (5 credits) - - Engineer: "Cards look good but margins are too large, reduce to 16px" - - Claude: - 4. Updates margins to 16px - 5. codevf-instant: "Applied 16px margins. Please verify the spacing - looks better now at https://abc123.loca.lt (password: xyz789)." (3 credits) - - Engineer: "Perfect! The spacing looks much cleaner now." - \`\`\` - -3. **Use the appropriate tool:** - - For instant queries: Call \`codevf-instant\` with the message and appropriate maxCredits (1-10) - - For extended sessions: Call \`codevf-chat\` with the message and appropriate maxCredits (suggest 240 for ~2 hours) - - For tunnel access: Call \`codevf-tunnel\` with the port number (e.g., { "port": 3000 }) - -4. **Present the response:** - - For instant queries: Share the engineer's response directly - - For chat sessions: Provide the session URL so the user can monitor the conversation - - For tunnels: Share the public URL that was created - -**Credit Guidelines:** -- Instant validation: 1-10 credits (typically 3-5 credits per question) -- UI verification: 5-8 credits for initial review, 3-5 credits for follow-up checks -- Extended chat: 2 credits per minute (240 credits = 2 hours) -- Tunnel creation: Free (no credits required) - -**Planning Integration for Complex Tasks:** -When Claude is planning multi-step implementations, especially UI-heavy tasks: -- Always include "Engineer verification checkpoints" in the plan -- Budget credits for iterative feedback (typically 15-25 credits total for UI refinement) -- Create tunnel early in the process for continuous visual validation -- Plan for 2-4 feedback cycles to achieve the desired result - -**Example Usage:** -- \`/cvf Does this authentication fix prevent the timing attack?\` → Use codevf-instant -- \`/cvf Complex race condition in WebSocket reconnection needs debugging\` → Use codevf-chat -- \`/cvf Create tunnel to my dev server on port 3000\` → Use codevf-tunnel -- \`/cvf Make the login form look more modern and user-friendly\` → Use tunnel + iterative codevf-instant feedback -`; From 0975795eed96f792935a63ab14e32eba3893e94a Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 09:14:16 +0800 Subject: [PATCH 7/9] fix: Pass token as Sec-WebSocket-Protocol header instead of URL parameter for security --- src/commands/watch-logs.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/commands/watch-logs.ts b/src/commands/watch-logs.ts index d72fe49..8e1dbeb 100644 --- a/src/commands/watch-logs.ts +++ b/src/commands/watch-logs.ts @@ -28,10 +28,18 @@ export async function watchLogsCommand(taskId?: string): Promise { const apiClient = new ApiClient(authManager); let wsClient: WebSocketClient | null = null; - // Connect to WebSocket try { - wsClient = new WebSocketClient(authManager); - await wsClient.connect(taskId); + const token = authManager.getAccessToken(); + if (!token) { + console.log('Error: No authentication token found'); + process.exit(1); + } + + const baseUrl = process.env.CODEVF_API_URL || 'http://localhost:3000'; + const wsUrl = `${baseUrl.replace(/^https?:/, 'ws:')}/ws?taskId=${taskId}&userType=customer`; + + wsClient = new WebSocketClient(wsUrl, token); + await wsClient.connect(); } catch (error) { console.error('Error connecting to WebSocket:', error); process.exit(1); @@ -79,10 +87,7 @@ export async function watchLogsCommand(taskId?: string): Promise { }); } -async function sendNewLogs( - state: FileWatchState, - wsClient: WebSocketClient | null -): Promise { +async function sendNewLogs(state: FileWatchState, wsClient: WebSocketClient | null): Promise { const logsPath = path.join(process.cwd(), 'logs.txt'); if (!fs.existsSync(logsPath) || !wsClient) { @@ -101,12 +106,10 @@ async function sendNewLogs( // Send via WebSocket wsClient.send({ payload: newContent, - type: 'customer_console_logs' + type: 'customer_console_logs', }); - console.log( - `Sent ${newContent.length} bytes of new logs to engineer` - ); + console.log(`Sent ${newContent.length} bytes of new logs to engineer`); } } catch (error) { // Silently ignore file read errors (file might be in use) From e7f9fd42fb21de66bdd7a10132ceb16695840776 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Thu, 8 Jan 2026 09:14:19 +0800 Subject: [PATCH 8/9] feat: Update WebSocketClient to accept optional token parameter as subprotocol --- src/lib/api/websocket.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/api/websocket.ts b/src/lib/api/websocket.ts index e957b74..b3a3fde 100644 --- a/src/lib/api/websocket.ts +++ b/src/lib/api/websocket.ts @@ -28,13 +28,15 @@ export interface BillingUpdate { export class WebSocketClient extends EventEmitter { private ws: WebSocket | null = null; private url: string; + private token: string | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private isClosedManually = false; - constructor(url: string) { + constructor(url: string, token?: string) { super(); this.url = url; + this.token = token || null; } /** @@ -44,7 +46,7 @@ export class WebSocketClient extends EventEmitter { return new Promise((resolve, reject) => { logger.info('Connecting to WebSocket', { url: this.url }); - this.ws = new WebSocket(this.url); + this.ws = new WebSocket(this.url, this.token ? [this.token] : undefined); this.ws.on('open', () => { logger.info('WebSocket connected'); From 9e5348046e1bfc2c8bec2ba16aac8c2601449b83 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Fri, 9 Jan 2026 00:05:53 +0800 Subject: [PATCH 9/9] feat: Implement log streaming with command execution in watchLogsCommand --- src/commands/log-wrapper.ts | 112 ++++++++++++++++++++++++++++++++++++ src/commands/watch-logs.ts | 108 +++++++++++++++++++++++++++++----- src/index.ts | 54 ++++++++--------- src/mcp/tools/chat.ts | 112 ++++++++++++++++++++++++++++++++---- 4 files changed, 330 insertions(+), 56 deletions(-) create mode 100644 src/commands/log-wrapper.ts diff --git a/src/commands/log-wrapper.ts b/src/commands/log-wrapper.ts new file mode 100644 index 0000000..217a2b7 --- /dev/null +++ b/src/commands/log-wrapper.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +/** + * Log Wrapper - Intercepts console output and writes properly formatted logs to logs.txt + * + * Usage: + * tsx src/commands/log-wrapper.ts "npm run dev" + * OR + * node log-wrapper.js "npm run build" + * + * This script: + * 1. Runs the specified command + * 2. Intercepts console.log/error/warn/info + * 3. Properly serializes objects with JSON.stringify + * 4. Writes to logs.txt with timestamps + */ + +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const logsPath = path.join(process.cwd(), 'logs.txt'); + +// Clear or create logs file +fs.writeFileSync(logsPath, '', 'utf-8'); + +function writeLog(level: string, args: any[]): void { + const timestamp = new Date().toISOString(); + + // Serialize each argument properly + const formattedArgs = args.map((arg) => { + if (typeof arg === 'string') { + return arg; + } else if (arg === null) { + return 'null'; + } else if (arg === undefined) { + return 'undefined'; + } else if (typeof arg === 'object') { + try { + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } else { + return String(arg); + } + }); + + const message = formattedArgs.join(' '); + const logLine = `[${timestamp}] [${level}] ${message}\n`; + + // Append to logs.txt + fs.appendFileSync(logsPath, logLine, 'utf-8'); + + // Also output to console for real-time feedback + console.log(logLine.trim()); +} + +// Get the command from arguments +const command = process.argv[2]; +if (!command) { + console.error('Usage: log-wrapper '); + console.error('Example: log-wrapper "npm run dev"'); + process.exit(1); +} + +// Parse command and arguments +const [cmd, ...cmdArgs] = command.split(' '); + +console.log(`Starting command: ${command}`); +console.log(`Logs will be written to: ${logsPath}\n`); + +// Spawn the child process +const child = spawn(cmd, cmdArgs, { + stdio: ['inherit', 'pipe', 'pipe'], + shell: true, +}); + +// Intercept stdout +if (child.stdout) { + child.stdout.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + writeLog('stdout', [line]); + } + } + }); +} + +// Intercept stderr +if (child.stderr) { + child.stderr.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + writeLog('stderr', [line]); + } + } + }); +} + +// Handle process exit +child.on('exit', (code) => { + writeLog('info', [`Process exited with code ${code}`]); + process.exit(code || 0); +}); + +child.on('error', (err) => { + writeLog('error', [`Failed to start process: ${err.message}`]); + process.exit(1); +}); diff --git a/src/commands/watch-logs.ts b/src/commands/watch-logs.ts index 8e1dbeb..f5a2288 100644 --- a/src/commands/watch-logs.ts +++ b/src/commands/watch-logs.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import { spawn } from 'child_process'; import { AuthManager } from '../modules/auth.js'; import { ApiClient } from '../modules/api.js'; import { handleError } from '../utils/errors.js'; @@ -10,7 +11,7 @@ interface FileWatchState { taskId: string; } -export async function watchLogsCommand(taskId?: string): Promise { +export async function watchLogsCommand(taskId?: string, command?: string): Promise { const authManager = new AuthManager(); if (!authManager.isAuthenticated()) { @@ -20,18 +21,27 @@ export async function watchLogsCommand(taskId?: string): Promise { if (!taskId) { console.log('Error: Task ID is required'); - console.log('Usage: codevf watch-logs '); + console.log('Usage: codevf watch-logs [command]'); + console.log('Example: codevf watch-logs abc123'); + console.log('Example with command: codevf watch-logs abc123 "npm run dev"'); process.exit(1); } const logsPath = path.join(process.cwd(), 'logs.txt'); - const apiClient = new ApiClient(authManager); let wsClient: WebSocketClient | null = null; + let childProcess: any = null; try { - const token = authManager.getAccessToken(); + let token: string | null = null; + try { + token = authManager.getAccessToken(); + } catch (authError) { + console.error('Error getting auth token:', authError); + process.exit(1); + } + if (!token) { - console.log('Error: No authentication token found'); + console.error('Error: No authentication token found'); process.exit(1); } @@ -45,10 +55,23 @@ export async function watchLogsCommand(taskId?: string): Promise { process.exit(1); } - // Check if logs.txt exists - if (!fs.existsSync(logsPath)) { - console.log(`logs.txt not found at ${logsPath}`); - console.log('Waiting for logs.txt to be created...'); + // Clear logs file if it exists + fs.writeFileSync(logsPath, '', 'utf-8'); + + // If a command is provided, run it with log interception + if (command) { + console.log(`Running command: ${command}`); + console.log(`Logs will be captured to: ${logsPath}\n`); + + childProcess = startCommandWithLogCapture(command, logsPath); + } else { + // Check if logs.txt exists + if (!fs.existsSync(logsPath)) { + console.log(`logs.txt not found at ${logsPath}`); + console.log('Waiting for logs.txt to be created...'); + } else { + console.log(`Watching logs.txt for task ${taskId}`); + } } const watchState: FileWatchState = { @@ -62,12 +85,13 @@ export async function watchLogsCommand(taskId?: string): Promise { watchState.lastPosition = Buffer.byteLength(content, 'utf-8'); } - console.log(`Watching logs.txt for task ${taskId}`); - console.log('Press Ctrl+C to stop watching'); + console.log(`Watching logs for task ${taskId}`); + console.log('Press Ctrl+C to stop'); - // Watch for file changes - const watcher = fs.watch(logsPath, async (eventType, filename) => { - if (eventType === 'change' && filename === 'logs.txt') { + // Watch for file changes - watch the directory instead if file doesn't exist yet + const watchDir = process.cwd(); + const watcher = fs.watch(watchDir, async (eventType, filename) => { + if (filename === 'logs.txt' && eventType === 'change') { try { await sendNewLogs(watchState, wsClient); } catch (error) { @@ -78,11 +102,14 @@ export async function watchLogsCommand(taskId?: string): Promise { // Handle graceful shutdown process.on('SIGINT', () => { - console.log('Stopping log watcher'); + console.log('\nStopping log watcher'); watcher.close(); if (wsClient) { wsClient.disconnect(); } + if (childProcess) { + childProcess.kill(); + } process.exit(0); }); } @@ -118,3 +145,54 @@ async function sendNewLogs(state: FileWatchState, wsClient: WebSocketClient | nu } } } + +/** + * Start a command and capture its output with proper object serialization + */ +function startCommandWithLogCapture(command: string, logsPath: string): any { + const logsFile = fs.createWriteStream(logsPath, { flags: 'a' }); + + // Parse command and arguments + const [cmd, ...cmdArgs] = command.split(' '); + + const child = spawn(cmd, cmdArgs, { + stdio: ['inherit', 'pipe', 'pipe'], + shell: true, + }); + + // Capture stdout + if (child.stdout) { + child.stdout.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + const logLine = `${new Date().toISOString()} [stdout] ${line}\n`; + logsFile.write(logLine); + process.stdout.write(line + '\n'); + } + } + }); + } + + // Capture stderr + if (child.stderr) { + child.stderr.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + const logLine = `${new Date().toISOString()} [stderr] ${line}\n`; + logsFile.write(logLine); + process.stderr.write(line + '\n'); + } + } + }); + } + + child.on('exit', (code) => { + const exitLine = `${new Date().toISOString()} [process] Exited with code ${code}\n`; + logsFile.write(exitLine); + logsFile.end(); + }); + + return child; +} diff --git a/src/index.ts b/src/index.ts index 4b894c8..355f9ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,11 +25,7 @@ import React from 'react'; import { render } from 'ink'; import { CLI_VERSION, RoutingMode, AgentMode } from './modules/constants.js'; import { InteractiveApp } from './ui/InteractiveApp.js'; -import { - renderHeader, - renderQuickCommands, - showModeSwitched, -} from './ui/SessionUI.js'; +import { renderHeader, renderQuickCommands, showModeSwitched } from './ui/SessionUI.js'; import { handleSlashCommand, isSlashCommand, @@ -39,10 +35,10 @@ import { } from './modules/commandHandler.js'; const args = hideBin(process.argv); -const SETUP_ONLY_MODE = process.env.SETUP_ONLY_MODE !== 'false'; +// SETUP_ONLY_MODE is for pure npx invocation; allow all commands with bun x or installed locally const isNpxInvocation = - process.env.npm_config_user_agent?.includes('npx') || - process.env.npm_execpath?.includes('npx'); + process.env.npm_config_user_agent?.includes('npx') || process.env.npm_execpath?.includes('npx'); +const SETUP_ONLY_MODE = isNpxInvocation && process.env.SETUP_ONLY_MODE !== 'false'; /** * Safely loads config without throwing errors @@ -75,10 +71,7 @@ async function runInteractiveMode() { // Display the user's input inside a full-width highlight block, left-aligned const columns = process.stdout.columns || 80; const lines = message.split(/\r?\n/); - const contentWidth = Math.min( - Math.max(...lines.map((line) => line.length), 1), - columns - 6 - ); + const contentWidth = Math.min(Math.max(...lines.map((line) => line.length), 1), columns - 6); const horizontalPad = ' '; const totalWidth = Math.min(columns, contentWidth + horizontalPad.length * 2); @@ -345,24 +338,7 @@ if (args.length === 0) { } } ) - .command( - 'watch-logs ', - 'Watch logs.txt and stream updates to an active chat session', - (yargs) => { - return yargs.positional('task-id', { - type: 'string', - describe: 'Task ID to send log updates to', - demandOption: true, - }); - }, - async (argv) => { - try { - await watchLogsCommand(argv['task-id'] as string); - } catch (error) { - handleError(error); - } - } - ) + .command( 'fix ', 'Start a live debugging session', @@ -455,6 +431,24 @@ if (args.length === 0) { } } ) + .command( + 'watch-logs ', + 'Watch logs.txt and stream updates to an active chat session', + (yargs) => { + return yargs.positional('task-id', { + type: 'string', + describe: 'Task ID to send log updates to', + demandOption: true, + }); + }, + async (argv) => { + try { + await watchLogsCommand(argv['task-id'] as string); + } catch (error) { + handleError(error); + } + } + ) .demandCommand( 1, chalk.yellow( diff --git a/src/mcp/tools/chat.ts b/src/mcp/tools/chat.ts index de86bca..6f5de50 100644 --- a/src/mcp/tools/chat.ts +++ b/src/mcp/tools/chat.ts @@ -231,6 +231,22 @@ export class ChatTool { // If there's a message, send it after ensuring the WebSocket is connected if (args.message) { logger.info('Sending message to existing session', { taskId: taskCheck.taskToResumeId }); + + // Wait a moment for WebSocket to be fully ready + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify connection before sending + if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) { + logger.error('WebSocket not ready for message send'); + return { + content: [{ + type: 'text', + text: 'Error: Failed to connect to chat session. Please try again.' + }], + isError: true + }; + } + this.sendWebSocketMessage(args.message, taskCheck.taskToResumeId); } logger.info('Waiting for engineer response via WebSocket...'); @@ -244,7 +260,7 @@ export class ChatTool { content: [ { type: 'text', - text: `Engineer Response:\n\n${engineerResponse}\n\n🚨 MANDATORY NEXT STEP 🚨\n\nYou MUST immediately call codevf-chat again with:\n- continueTaskId: "${taskCheck.taskToResumeId}"\n- message: Describe what you did in response to the engineer's instructions\n- previouslyConnected: true\n\nDO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.`, + text: `Engineer Response:\n\n${engineerResponse}\n\n🚨 MANDATORY NEXT STEP 🚨\n\nYou MUST immediately call codevf-chat again with:\n- continueTaskId: "${taskCheck.taskToResumeId}"\n- message: Describe what you did in response to the engineer's instructions\n- previouslyConnected: true\n\nIMPORTANT: When running any app/command:\nRun: bun x codevf watch-logs ${taskCheck.taskToResumeId} "[your command]"\nExample: bun x codevf watch-logs ${taskCheck.taskToResumeId} "npm run dev"\n(or: codevf watch-logs ${taskCheck.taskToResumeId} "npm run dev" if using npm/yarn)\n\nThis captures all logs (including objects) and streams them to the engineer in real-time.\n\nDO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.`, }, ], }; @@ -435,6 +451,11 @@ export class ChatTool { finalResponse += `- continueTaskId: "${task.taskId}"\n`; finalResponse += `- message: Describe what you did in response to the engineer's instructions\n`; finalResponse += `- previouslyConnected: true\n\n`; + finalResponse += `IMPORTANT: When running any app/command:\n`; + finalResponse += `Run: bun x codevf watch-logs ${task.taskId} "[your command]"\n`; + finalResponse += `Example: bun x codevf watch-logs ${task.taskId} "npm run dev"\n`; + finalResponse += `(or: codevf watch-logs ${task.taskId} "npm run dev" if using npm/yarn)\n\n`; + finalResponse += `This captures all logs (including objects) and streams them to the engineer in real-time.\n\n`; finalResponse += `DO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.\n\n`; finalResponse += `Example: codevf-chat with message="I've completed [task]: [results]" and continueTaskId="${task.taskId}"`; @@ -606,6 +627,32 @@ export class ChatTool { case 'customer_message': case 'engineer_message': + // Log raw payload for debugging + logger.info('Raw engineer_message received', { + payloadKeys: Object.keys(message.payload || {}), + payloadType: message.payload?.type, + fullPayload: JSON.stringify(message.payload), + }); + + // Check if this is a template command + if (message.payload?.type === 'template_command') { + const template = message.payload?.template; + const command = message.payload?.command; + const context = message.payload?.context; + const instructions = message.payload?.instructions; + + logger.info('Engineer sent template command', { + template, + command, + contextPreview: context?.substring(0, 100), + }); + + // Process template command with full context + const templateMessage = `[TEMPLATE COMMAND: ${template}]\n\nCommand: ${command}\n\nContext: ${context}\n\nInstructions: ${instructions}`; + this.analyzeAndRespond(templateMessage, 'engineer', taskId); + break; + } + // Log the conversation for context const sender = message.payload?.sender || message.type.replace('_message', ''); // Support both payload.content and payload.message for backwards compatibility @@ -643,10 +690,16 @@ export class ChatTool { /** * Send a message via WebSocket */ - private sendWebSocketMessage(content: string, taskId: string): void { + private sendWebSocketMessage(content: string, taskId: string, additionalMetadata?: Record): boolean { + // Validate message content + if (!content || typeof content !== 'string') { + logger.error('Invalid message content', { content }); + return false; + } + if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { - this.wsConnection.send( - JSON.stringify({ + try { + const message = JSON.stringify({ type: 'ai_assistant_message', timestamp: new Date().toISOString(), payload: { @@ -654,14 +707,27 @@ export class ChatTool { metadata: { source: 'claude-mcp', taskId, + ...additionalMetadata, }, }, - }) - ); - logger.info('Claude sent message via WebSocket', { - taskId, - preview: content.substring(0, 50), - }); + }); + + this.wsConnection.send(message); + + logger.info('Claude sent message via WebSocket', { + taskId, + preview: content.substring(0, 50), + messageLength: content.length, + }); + + return true; + } catch (error) { + logger.error('Failed to send WebSocket message', { + error: (error as Error).message, + taskId, + }); + return false; + } } else { logger.warn('Failed to send WebSocket message: connection is not open', { taskId, @@ -669,6 +735,7 @@ export class ChatTool { readyState: this.wsConnection?.readyState, preview: content.substring(0, 50), }); + return false; } } @@ -694,7 +761,25 @@ export class ChatTool { if (sender === 'engineer' && this.responseResolver) { // Send acknowledgment before disconnecting const acknowledgment = "Got it! Working on this now. I'll report back once complete."; - this.sendWebSocketMessage(acknowledgment, taskId); + + // Send acknowledgment with metadata flag + if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { + this.wsConnection.send( + JSON.stringify({ + type: 'ai_assistant_message', + timestamp: new Date().toISOString(), + payload: { + content: acknowledgment, + metadata: { + source: 'claude-mcp', + taskId, + isAcknowledgment: true, // Flag to distinguish from real responses + }, + }, + }) + ); + logger.info('Sent acknowledgment via WebSocket', { taskId }); + } logger.info('Sent acknowledgment, preparing to disconnect and work', { taskId }); @@ -820,7 +905,12 @@ export class ChatTool { this.wsConnection.close(); this.wsConnection = null; } + + // Clear all state to prevent message carryover this.messageBuffer = []; this.currentTaskId = null; + this.hasConnected = false; + + logger.info('WebSocket disconnected and state cleared'); } }