2026-04-25 11:58:29 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
import { spawn } from 'child_process';
|
2026-04-25 15:47:49 +00:00
|
|
|
import fs from 'fs';
|
2026-04-25 11:58:29 +00:00
|
|
|
import os from 'os';
|
|
|
|
|
import path from 'path';
|
2026-04-25 15:47:49 +00:00
|
|
|
import { type GenerationJob, GenerationJobCancelledError } from './generation-job-manager';
|
2026-04-25 11:58:29 +00:00
|
|
|
import { parseCardsJson } from './schema';
|
2026-04-25 15:47:49 +00:00
|
|
|
import type { PluginSettings, RawCard } from './types';
|
2026-04-25 11:58:29 +00:00
|
|
|
|
|
|
|
|
/* Obsidian launched from GUI doesn't inherit shell PATH. */
|
refactor: add TypeScript interfaces for core types
Create src/types.ts with RawCard, ResolvedCard, CardPatch, CacheEntry,
PluginSettings, ApiProviderPreset, ApiFormat, GenerationPhase, ErrorKind,
and PromptPair. Update all source modules to use typed signatures,
replacing `any` with concrete interfaces.
Change-Id: I87b7dd6f3a240c95597d7a796b4d1885386aa632
2026-04-25 15:34:45 +00:00
|
|
|
export function resolveCliPath(name: string, override: string): string {
|
2026-04-25 15:47:49 +00:00
|
|
|
if (override?.trim()) return override.trim();
|
2026-04-25 11:58:29 +00:00
|
|
|
const home = os.homedir();
|
|
|
|
|
const candidates = [
|
|
|
|
|
path.join(home, 'bin', name),
|
|
|
|
|
path.join(home, '.local/bin', name),
|
|
|
|
|
path.join(home, '.claude/local', name),
|
|
|
|
|
path.join(home, '.codex/bin', name),
|
|
|
|
|
path.join(home, '.bun/bin', name),
|
|
|
|
|
path.join(home, '.npm-global/bin', name),
|
|
|
|
|
path.join(home, '.cargo/bin', name),
|
|
|
|
|
'/opt/homebrew/bin/' + name,
|
|
|
|
|
'/usr/local/bin/' + name,
|
|
|
|
|
];
|
|
|
|
|
for (const p of candidates) {
|
|
|
|
|
try {
|
|
|
|
|
if (fs.existsSync(p)) return p;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Ignore unreadable candidate paths and keep searching.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 15:47:49 +00:00
|
|
|
export function runCli(
|
|
|
|
|
cmd: string,
|
|
|
|
|
args: string[],
|
|
|
|
|
stdinText: string,
|
|
|
|
|
timeoutMs: number,
|
|
|
|
|
job?: GenerationJob,
|
|
|
|
|
): Promise<{ stdout: string; stderr: string }> {
|
2026-04-25 11:58:29 +00:00
|
|
|
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
let child: ReturnType<typeof spawn>;
|
2026-04-25 11:58:29 +00:00
|
|
|
let settled = false;
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
const fail = (err: Error) => {
|
2026-04-25 11:58:29 +00:00
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
reject(err);
|
|
|
|
|
};
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
const succeed = (value: { stdout: string; stderr: string }) => {
|
2026-04-25 11:58:29 +00:00
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
resolve(value);
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
child = spawn(cmd, args, {
|
|
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
|
|
|
env: {
|
|
|
|
|
...process.env,
|
|
|
|
|
PATH: [
|
|
|
|
|
process.env.PATH || '',
|
|
|
|
|
'/usr/local/bin',
|
|
|
|
|
'/opt/homebrew/bin',
|
|
|
|
|
path.join(os.homedir(), '.local/bin'),
|
|
|
|
|
path.join(os.homedir(), '.claude/local'),
|
2026-04-25 15:47:49 +00:00
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(':'),
|
2026-04-25 11:58:29 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return reject(new Error(`Failed to start ${cmd}: ${e.message}`));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stderr = '';
|
|
|
|
|
timer = setTimeout(() => {
|
2026-04-25 15:47:49 +00:00
|
|
|
try {
|
|
|
|
|
child.kill('SIGKILL');
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
2026-04-25 11:58:29 +00:00
|
|
|
fail(new Error(`CLI timed out (${timeoutMs}ms)`));
|
|
|
|
|
}, timeoutMs);
|
|
|
|
|
if (job) {
|
|
|
|
|
job.onCancel(() => {
|
2026-04-25 15:47:49 +00:00
|
|
|
try {
|
|
|
|
|
child.kill('SIGKILL');
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
2026-04-25 11:58:29 +00:00
|
|
|
fail(new GenerationJobCancelledError(job.key));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
child.stdout!.on('data', (d) => {
|
2026-04-25 15:47:49 +00:00
|
|
|
stdout += d.toString('utf8');
|
|
|
|
|
});
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
child.stderr!.on('data', (d) => {
|
2026-04-25 15:47:49 +00:00
|
|
|
stderr += d.toString('utf8');
|
|
|
|
|
});
|
|
|
|
|
child.on('error', (e) => {
|
2026-04-25 11:58:29 +00:00
|
|
|
fail(new Error(`CLI startup error: ${e.message}. Try setting an absolute CLI path.`));
|
|
|
|
|
});
|
2026-04-25 15:47:49 +00:00
|
|
|
child.on('close', (code) => {
|
2026-04-25 11:58:29 +00:00
|
|
|
if (settled) return;
|
|
|
|
|
if (code !== 0) {
|
|
|
|
|
return fail(new Error(`CLI exited with code ${code}\nstderr:\n${stderr.slice(0, 1000)}`));
|
|
|
|
|
}
|
|
|
|
|
succeed({ stdout, stderr });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (stdinText) {
|
|
|
|
|
try {
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
child.stdin!.write(stdinText);
|
|
|
|
|
child.stdin!.end();
|
2026-04-25 15:47:49 +00:00
|
|
|
} catch (_e) {
|
2026-04-25 11:58:29 +00:00
|
|
|
// Child may have exited before stdin was written.
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-04-25 15:47:49 +00:00
|
|
|
try {
|
refactor: enable noImplicitAny and add type annotations
Turn on noImplicitAny in tsconfig.json. Add explicit type annotations
to all 137 previously untyped parameters across main.ts, view.ts,
modal.ts, settings-tab.ts, providers.ts, cli.ts, anchor.ts, scroll.ts,
ui-helpers.ts, vault.ts, and settings.ts.
Change-Id: Icc5a69ab57113e9f6dc08dd97e3a8fa5e7e24937
2026-04-26 00:58:41 +00:00
|
|
|
child.stdin!.end();
|
2026-04-25 15:47:49 +00:00
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
2026-04-25 11:58:29 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 15:47:49 +00:00
|
|
|
export async function summarizeViaClaudeCode(
|
|
|
|
|
system: string,
|
|
|
|
|
user: string,
|
|
|
|
|
settings: PluginSettings,
|
|
|
|
|
job?: GenerationJob,
|
|
|
|
|
): Promise<RawCard[]> {
|
2026-04-25 11:58:29 +00:00
|
|
|
const cmd = resolveCliPath('claude', settings.cliPath);
|
|
|
|
|
const args = [
|
|
|
|
|
'-p',
|
2026-04-25 15:47:49 +00:00
|
|
|
'--output-format',
|
|
|
|
|
'json',
|
|
|
|
|
'--append-system-prompt',
|
|
|
|
|
system,
|
|
|
|
|
'--disallowed-tools',
|
|
|
|
|
'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task',
|
2026-04-25 11:58:29 +00:00
|
|
|
];
|
|
|
|
|
if (settings.model) {
|
|
|
|
|
args.push('--model', settings.model);
|
|
|
|
|
}
|
|
|
|
|
const { stdout } = await runCli(cmd, args, user, settings.cliTimeoutMs, job);
|
|
|
|
|
|
|
|
|
|
let envelope;
|
|
|
|
|
try {
|
|
|
|
|
envelope = JSON.parse(stdout);
|
2026-04-25 15:47:49 +00:00
|
|
|
} catch (_e) {
|
2026-04-25 11:58:29 +00:00
|
|
|
throw new Error('claude CLI returned a non-JSON envelope:\n' + stdout.slice(0, 500));
|
|
|
|
|
}
|
|
|
|
|
const resultText = envelope.result || envelope.content || '';
|
|
|
|
|
return parseCardsJson(resultText, settings);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 15:47:49 +00:00
|
|
|
export async function summarizeViaCodex(
|
|
|
|
|
system: string,
|
|
|
|
|
user: string,
|
|
|
|
|
settings: PluginSettings,
|
|
|
|
|
job?: GenerationJob,
|
|
|
|
|
): Promise<RawCard[]> {
|
2026-04-25 11:58:29 +00:00
|
|
|
const cmd = resolveCliPath('codex', settings.cliPath);
|
|
|
|
|
const combined = `<<SYSTEM>>\n${system}\n<<USER>>\n${user}\n\nOutput JSON directly with no explanation.`;
|
|
|
|
|
const args = ['exec', '--skip-git-repo-check', '-'];
|
|
|
|
|
const { stdout } = await runCli(cmd, args, combined, settings.cliTimeoutMs, job);
|
|
|
|
|
return parseCardsJson(stdout, settings);
|
|
|
|
|
}
|