From 3fd34871642cca4183cc98ce15cc4f697042ea22 Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Mon, 23 Mar 2026 10:03:10 -0400 Subject: [PATCH] Fix Claude Code Windows process launch --- .../external/ClaudeCodeAuthService.ts | 15 ++++++- .../external/ClaudeHeadlessService.ts | 29 +++++++++--- .../AnthropicClaudeCodeAdapter.ts | 28 +++++++++--- src/utils/binaryDiscovery.ts | 44 ++++++++++++++++--- src/utils/connectorContent.ts | 2 +- src/utils/desktopProcess.ts | 21 +++++++++ 6 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/utils/desktopProcess.ts diff --git a/src/services/external/ClaudeCodeAuthService.ts b/src/services/external/ClaudeCodeAuthService.ts index 8363d0281..f3715a4c5 100644 --- a/src/services/external/ClaudeCodeAuthService.ts +++ b/src/services/external/ClaudeCodeAuthService.ts @@ -1,5 +1,6 @@ import { App, FileSystemAdapter, Platform } from 'obsidian'; import { resolveDesktopBinaryPath } from '../../utils/binaryDiscovery'; +import { spawnDesktopProcess } from '../../utils/desktopProcess'; export interface ClaudeCodeAuthStatus { available: boolean; @@ -73,7 +74,8 @@ export class ClaudeCodeAuthService { } const childProcess = require('child_process') as typeof import('child_process'); - const child = childProcess.spawn( + const child = spawnDesktopProcess( + childProcess, initialStatus.claudePath, ['auth', 'login', '--claudeai'], { @@ -122,12 +124,21 @@ export class ClaudeCodeAuthService { const childProcess = require('child_process') as typeof import('child_process'); return await new Promise((resolve) => { - const child = childProcess.spawn(command, args, { + const child = spawnDesktopProcess(childProcess, command, args, { cwd, env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'] }); + if (!child.stdout || !child.stderr) { + resolve({ + stdout: '', + stderr: 'Failed to capture Claude Code process output.', + exitCode: null + }); + return; + } + let stdout = ''; let stderr = ''; diff --git a/src/services/external/ClaudeHeadlessService.ts b/src/services/external/ClaudeHeadlessService.ts index aecb51094..9d2da2ef0 100644 --- a/src/services/external/ClaudeHeadlessService.ts +++ b/src/services/external/ClaudeHeadlessService.ts @@ -1,6 +1,7 @@ import { App, FileSystemAdapter, Plugin, Platform } from 'obsidian'; import { getPrimaryServerKey } from '../../constants/branding'; import { resolveDesktopBinaryPath } from '../../utils/binaryDiscovery'; +import { spawnDesktopProcess } from '../../utils/desktopProcess'; export interface ClaudeHeadlessPreflightResult { claudePath: string | null; @@ -139,13 +140,12 @@ export class ClaudeHeadlessService { args.push('--model', model); } - args.push(prompt); - const processResult = await this.runProcess( preflight.claudePath, args, preflight.vaultPath, - this.buildClaudeEnv() + this.buildClaudeEnv(), + prompt ); return { @@ -208,20 +208,35 @@ export class ClaudeHeadlessService { command: string, args: string[], cwd?: string, - env?: NodeJS.ProcessEnv + env?: NodeJS.ProcessEnv, + stdinText?: string ): Promise { const childProcess = require('child_process') as typeof import('child_process'); return await new Promise((resolve) => { - const child = childProcess.spawn(command, args, { + const child = spawnDesktopProcess(childProcess, command, args, { cwd, env, - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['pipe', 'pipe', 'pipe'] }); + if (!child.stdin || !child.stdout || !child.stderr) { + resolve({ + stdout: '', + stderr: 'Failed to attach Claude Code process stdio.', + exitCode: null + }); + return; + } + let stdout = ''; let stderr = ''; + if (stdinText) { + child.stdin.write(stdinText); + } + child.stdin.end(); + child.stdout.on('data', (chunk: Buffer | string) => { stdout += chunk.toString(); }); @@ -256,7 +271,7 @@ export class ClaudeHeadlessService { const pathMod = require('path') as typeof import('path'); const manifestDir = this.plugin.manifest.dir; - const pluginFolderName = manifestDir ? manifestDir.split('/').pop() || manifestDir : ''; + const pluginFolderName = manifestDir ? pathMod.basename(manifestDir) : ''; if (!pluginFolderName) { return null; diff --git a/src/services/llm/adapters/anthropic-claude-code/AnthropicClaudeCodeAdapter.ts b/src/services/llm/adapters/anthropic-claude-code/AnthropicClaudeCodeAdapter.ts index 93879d459..933ded7c9 100644 --- a/src/services/llm/adapters/anthropic-claude-code/AnthropicClaudeCodeAdapter.ts +++ b/src/services/llm/adapters/anthropic-claude-code/AnthropicClaudeCodeAdapter.ts @@ -1,6 +1,7 @@ import { FileSystemAdapter, Vault } from 'obsidian'; import { BaseAdapter } from '../BaseAdapter'; import { resolveDesktopBinaryPath } from '../../../../utils/binaryDiscovery'; +import { spawnDesktopProcess } from '../../../../utils/desktopProcess'; import { GenerateOptions, StreamChunk, @@ -127,23 +128,31 @@ export class AnthropicClaudeCodeAdapter extends BaseAdapter { args.push('--model', model); } - args.push(prompt); - const env = { ...process.env }; delete env.ANTHROPIC_API_KEY; delete env.ANTHROPIC_AUTH_TOKEN; - const child = childProcess.spawn(runtime.claudePath, args, { + const child = spawnDesktopProcess(childProcess, runtime.claudePath, args, { cwd: runtime.vaultPath, env, - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['pipe', 'pipe', 'pipe'] }); + if (!child.stdin || !child.stdout || !child.stderr) { + throw new LLMProviderError( + 'Failed to attach Claude Code process stdio.', + this.name, + 'PROVIDER_ERROR' + ); + } const closePromise = new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>((resolve) => { child.on('close', (exitCode: number | null, signal: NodeJS.Signals | null) => { resolve({ exitCode, signal }); }); }); + child.stdin.write(prompt); + child.stdin.end(); + child.stderr.on('data', (chunk: Buffer | string) => { stderr += chunk.toString(); }); @@ -498,12 +507,21 @@ export class AnthropicClaudeCodeAdapter extends BaseAdapter { delete env.ANTHROPIC_API_KEY; delete env.ANTHROPIC_AUTH_TOKEN; - const child = childProcess.spawn(command, args, { + const child = spawnDesktopProcess(childProcess, command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }); + if (!child.stdout || !child.stderr) { + resolve({ + stdout: '', + stderr: 'Failed to capture Claude Code process output.', + exitCode: null + }); + return; + } + let stdout = ''; let stderr = ''; diff --git a/src/utils/binaryDiscovery.ts b/src/utils/binaryDiscovery.ts index a606b375e..d1f9827f4 100644 --- a/src/utils/binaryDiscovery.ts +++ b/src/utils/binaryDiscovery.ts @@ -16,6 +16,8 @@ const COMMON_WINDOWS_BIN_DIRS = [ 'C:\\Program Files\\Anthropic\\Claude' ]; +const WINDOWS_BINARY_PRIORITY = ['.exe', '.cmd', '.bat', '.com', '']; + export function resolveDesktopBinaryPath(binaryName: string): string | null { if (!Platform.isDesktop) { return null; @@ -38,16 +40,23 @@ function resolveFromCurrentPath(binaryName: string): string | null { try { const childProcess = require('child_process') as typeof import('child_process'); const nodeFs = require('fs') as typeof import('fs'); - const command = Platform.isWin ? `where ${binaryName}` : `which ${binaryName}`; + const command = Platform.isWin ? `where.exe ${binaryName}` : `which ${binaryName}`; const result = childProcess.execSync(command, { encoding: 'utf8', timeout: 5000, env: { ...process.env } - }).trim(); - - const firstLine = result.split(/\r?\n/u)[0]?.trim(); - if (firstLine && nodeFs.existsSync(firstLine)) { - return firstLine; + }); + + const candidates = result + .split(/\r?\n/u) + .map((line: string) => line.trim()) + .filter(Boolean) + .filter((candidate: string) => nodeFs.existsSync(candidate)); + + if (candidates.length > 0) { + return Platform.isWin + ? chooseBestWindowsCandidate(candidates) + : candidates[0] ?? null; } } catch { // Fall through to deterministic location checks. @@ -61,7 +70,9 @@ function resolveFromCommonLocations(binaryName: string): string | null { const nodeFs = require('fs') as typeof import('fs'); const pathMod = require('path') as typeof import('path'); const binDirs = Platform.isWin ? COMMON_WINDOWS_BIN_DIRS : COMMON_UNIX_BIN_DIRS; - const candidateNames = Platform.isWin ? [binaryName, `${binaryName}.exe`, `${binaryName}.cmd`] : [binaryName]; + const candidateNames = Platform.isWin + ? [`${binaryName}.exe`, `${binaryName}.cmd`, `${binaryName}.bat`, binaryName] + : [binaryName]; for (const dir of binDirs) { for (const candidateName of candidateNames) { @@ -78,6 +89,25 @@ function resolveFromCommonLocations(binaryName: string): string | null { return null; } +function chooseBestWindowsCandidate(candidates: string[]): string | null { + const pathMod = require('path') as typeof import('path'); + + const ranked = [...candidates].sort((left, right) => { + const leftScore = WINDOWS_BINARY_PRIORITY.indexOf(pathMod.extname(left).toLowerCase()); + const rightScore = WINDOWS_BINARY_PRIORITY.indexOf(pathMod.extname(right).toLowerCase()); + const normalizedLeft = leftScore === -1 ? Number.MAX_SAFE_INTEGER : leftScore; + const normalizedRight = rightScore === -1 ? Number.MAX_SAFE_INTEGER : rightScore; + + if (normalizedLeft !== normalizedRight) { + return normalizedLeft - normalizedRight; + } + + return left.localeCompare(right); + }); + + return ranked[0] ?? null; +} + function resolveFromLoginShell(binaryName: string): string | null { if (Platform.isWin) { return null; diff --git a/src/utils/connectorContent.ts b/src/utils/connectorContent.ts index 44c194d4f..896194805 100644 --- a/src/utils/connectorContent.ts +++ b/src/utils/connectorContent.ts @@ -5,7 +5,7 @@ * DO NOT EDIT MANUALLY - This file is regenerated during the build process. * To update, modify connector.ts and rebuild. * - * Generated: 2026-03-23T12:31:33.701Z + * Generated: 2026-03-23T14:02:32.000Z */ export const CONNECTOR_JS_CONTENT = `"use strict"; diff --git a/src/utils/desktopProcess.ts b/src/utils/desktopProcess.ts new file mode 100644 index 000000000..f5c17791f --- /dev/null +++ b/src/utils/desktopProcess.ts @@ -0,0 +1,21 @@ +import { Platform } from 'obsidian'; + +type ChildProcessModule = typeof import('child_process'); +type SpawnOptions = import('child_process').SpawnOptions; + +function isWindowsCommandWrapper(command: string): boolean { + return Platform.isWin && /\.(cmd|bat)$/iu.test(command); +} + +export function spawnDesktopProcess( + childProcess: ChildProcessModule, + command: string, + args: string[], + options: SpawnOptions +) { + return childProcess.spawn(command, args, { + ...options, + shell: options.shell ?? isWindowsCommandWrapper(command), + windowsHide: true + }); +}