Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/services/external/ClaudeCodeAuthService.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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'],
{
Expand Down Expand Up @@ -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 = '';

Expand Down
29 changes: 22 additions & 7 deletions src/services/external/ClaudeHeadlessService.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -208,20 +208,35 @@ export class ClaudeHeadlessService {
command: string,
args: string[],
cwd?: string,
env?: NodeJS.ProcessEnv
env?: NodeJS.ProcessEnv,
stdinText?: string
): Promise<ProcessResult> {
const childProcess = require('child_process') as typeof import('child_process');

return await new Promise<ProcessResult>((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();
});
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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 = '';

Expand Down
44 changes: 37 additions & 7 deletions src/utils/binaryDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/connectorContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
21 changes: 21 additions & 0 deletions src/utils/desktopProcess.ts
Original file line number Diff line number Diff line change
@@ -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
});
}