diff --git a/apps/server/src/routes/claude/index.ts b/apps/server/src/routes/claude/index.ts index 20816bbc..ec35ca1b 100644 --- a/apps/server/src/routes/claude/index.ts +++ b/apps/server/src/routes/claude/index.ts @@ -34,6 +34,13 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router { error: 'Authentication required', message: "Please run 'claude login' to authenticate", }); + } else if (message.includes('TRUST_PROMPT_PENDING')) { + // Trust prompt appeared but couldn't be auto-approved + res.status(200).json({ + error: 'Trust prompt pending', + message: + 'Claude CLI needs folder permission. Please run "claude" in your terminal and approve access.', + }); } else if (message.includes('timed out')) { res.status(200).json({ error: 'Command timed out', diff --git a/apps/server/src/services/claude-usage-service.ts b/apps/server/src/services/claude-usage-service.ts index c9000582..64dceb6a 100644 --- a/apps/server/src/services/claude-usage-service.ts +++ b/apps/server/src/services/claude-usage-service.ts @@ -49,13 +49,11 @@ export class ClaudeUsageService { /** * Execute the claude /usage command and return the output - * Uses platform-specific PTY implementation + * Uses node-pty on all platforms for consistency */ private executeClaudeUsageCommand(): Promise { - if (this.isWindows || this.isLinux) { - return this.executeClaudeUsageCommandPty(); - } - return this.executeClaudeUsageCommandMac(); + // Use node-pty on all platforms - it's more reliable than expect on macOS + return this.executeClaudeUsageCommandPty(); } /** @@ -67,24 +65,36 @@ export class ClaudeUsageService { let stderr = ''; let settled = false; - // Use a simple working directory (home or tmp) - const workingDirectory = process.env.HOME || '/tmp'; + // Use current working directory - likely already trusted by Claude CLI + const workingDirectory = process.cwd(); // Use 'expect' with an inline script to run claude /usage with a PTY - // Wait for "Current session" header, then wait for full output before exiting + // Running from cwd which should already be trusted const expectScript = ` - set timeout 20 + set timeout 30 spawn claude /usage + + # Wait for usage data or handle trust prompt if needed expect { - "Current session" { - sleep 2 - send "\\x1b" + -re "Ready to code|permission to work|Do you want to work" { + # Trust prompt appeared - send Enter to approve + sleep 1 + send "\\r" + exp_continue } - "Esc to cancel" { + "Current session" { + # Usage data appeared - wait for full output, then exit sleep 3 send "\\x1b" } - timeout {} + "% left" { + # Usage percentage appeared + sleep 3 + send "\\x1b" + } + timeout { + send "\\x1b" + } eof {} } expect eof @@ -158,10 +168,10 @@ export class ClaudeUsageService { let output = ''; let settled = false; let hasSeenUsageData = false; + let hasSeenTrustPrompt = false; - const workingDirectory = this.isWindows - ? process.env.USERPROFILE || os.homedir() || 'C:\\' - : os.tmpdir(); + // Use current working directory (project dir) - most likely already trusted by Claude CLI + const workingDirectory = process.cwd(); // Use platform-appropriate shell and command const shell = this.isWindows ? 'cmd.exe' : '/bin/sh'; @@ -206,6 +216,13 @@ export class ClaudeUsageService { // Don't fail if we have data - return it instead if (output.includes('Current session')) { resolve(output); + } else if (hasSeenTrustPrompt) { + // Trust prompt was shown but we couldn't auto-approve it + reject( + new Error( + 'TRUST_PROMPT_PENDING: Claude CLI is waiting for folder permission. Please run "claude" in your terminal and approve access to continue.' + ) + ); } else { reject( new Error( @@ -269,10 +286,18 @@ export class ClaudeUsageService { }, 3000); } - // Handle Trust Dialog: "Do you want to work in this folder?" - // Since we are running in os.tmpdir(), it is safe to approve. - if (!hasApprovedTrust && cleanOutput.includes('Do you want to work in this folder?')) { + // Handle Trust Dialog - multiple variants: + // - "Do you want to work in this folder?" + // - "Ready to code here?" / "I'll need permission to work with your files" + // Since we are running in cwd (project dir), it is safe to approve. + if ( + !hasApprovedTrust && + (cleanOutput.includes('Do you want to work in this folder?') || + cleanOutput.includes('Ready to code here') || + cleanOutput.includes('permission to work with your files')) + ) { hasApprovedTrust = true; + hasSeenTrustPrompt = true; // Wait a tiny bit to ensure prompt is ready, then send Enter setTimeout(() => { if (!settled && ptyProcess && !ptyProcess.killed) { diff --git a/apps/ui/src/components/claude-usage-popover.tsx b/apps/ui/src/components/claude-usage-popover.tsx index 227f16e1..d51e316c 100644 --- a/apps/ui/src/components/claude-usage-popover.tsx +++ b/apps/ui/src/components/claude-usage-popover.tsx @@ -11,6 +11,7 @@ import { useSetupStore } from '@/store/setup-store'; const ERROR_CODES = { API_BRIDGE_UNAVAILABLE: 'API_BRIDGE_UNAVAILABLE', AUTH_ERROR: 'AUTH_ERROR', + TRUST_PROMPT: 'TRUST_PROMPT', UNKNOWN: 'UNKNOWN', } as const; @@ -55,8 +56,12 @@ export function ClaudeUsagePopover() { } const data = await api.claude.getUsage(); if ('error' in data) { + // Detect trust prompt error + const isTrustPrompt = + data.error === 'Trust prompt pending' || + (data.message && data.message.includes('folder permission')); setError({ - code: ERROR_CODES.AUTH_ERROR, + code: isTrustPrompt ? ERROR_CODES.TRUST_PROMPT : ERROR_CODES.AUTH_ERROR, message: data.message || data.error, }); return; @@ -257,6 +262,11 @@ export function ClaudeUsagePopover() {

{error.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? ( 'Ensure the Electron bridge is running or restart the app' + ) : error.code === ERROR_CODES.TRUST_PROMPT ? ( + <> + Run claude in your + terminal and approve access to continue + ) : ( <> Make sure Claude CLI is installed and authenticated via{' '} diff --git a/apps/ui/src/components/usage-popover.tsx b/apps/ui/src/components/usage-popover.tsx index 1194cb9c..fa8a6a1b 100644 --- a/apps/ui/src/components/usage-popover.tsx +++ b/apps/ui/src/components/usage-popover.tsx @@ -14,6 +14,7 @@ const ERROR_CODES = { API_BRIDGE_UNAVAILABLE: 'API_BRIDGE_UNAVAILABLE', AUTH_ERROR: 'AUTH_ERROR', NOT_AVAILABLE: 'NOT_AVAILABLE', + TRUST_PROMPT: 'TRUST_PROMPT', UNKNOWN: 'UNKNOWN', } as const; @@ -108,8 +109,12 @@ export function UsagePopover() { } const data = await api.claude.getUsage(); if ('error' in data) { + // Detect trust prompt error + const isTrustPrompt = + data.error === 'Trust prompt pending' || + (data.message && data.message.includes('folder permission')); setClaudeError({ - code: ERROR_CODES.AUTH_ERROR, + code: isTrustPrompt ? ERROR_CODES.TRUST_PROMPT : ERROR_CODES.AUTH_ERROR, message: data.message || data.error, }); return; @@ -404,6 +409,11 @@ export function UsagePopover() {

{claudeError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? ( 'Ensure the Electron bridge is running or restart the app' + ) : claudeError.code === ERROR_CODES.TRUST_PROMPT ? ( + <> + Run claude in + your terminal and approve access to continue + ) : ( <> Make sure Claude CLI is installed and authenticated via{' '}