@albireo3754/agentlog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Claude Code settings.json hook management.
3
+ * Centralizes read/write/query of the agentlog hook entry
4
+ * in ~/.claude/settings.json.
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ export const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
12
+
13
+ export const HOOK_ENTRY = {
14
+ matcher: "",
15
+ hooks: [
16
+ {
17
+ type: "command",
18
+ command: "agentlog hook",
19
+ },
20
+ ],
21
+ };
22
+
23
+ /** Returns true if the given hook entry contains an "agentlog hook" command. */
24
+ function isAgentlogHookEntry(entry: unknown): boolean {
25
+ return (
26
+ typeof entry === "object" &&
27
+ entry !== null &&
28
+ Array.isArray((entry as Record<string, unknown>)["hooks"]) &&
29
+ ((entry as Record<string, unknown>)["hooks"] as unknown[]).some(
30
+ (h) =>
31
+ typeof h === "object" &&
32
+ h !== null &&
33
+ (h as Record<string, unknown>)["command"] === "agentlog hook"
34
+ )
35
+ );
36
+ }
37
+
38
+ function readClaudeSettings(): Record<string, unknown> | null {
39
+ if (!existsSync(CLAUDE_SETTINGS_PATH)) return null;
40
+ try {
41
+ return JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function writeClaudeSettings(settings: Record<string, unknown>): void {
48
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
49
+ }
50
+
51
+ export function unregisterHook(): boolean {
52
+ const settings = readClaudeSettings();
53
+ if (!settings) return false;
54
+
55
+ const hooks = settings["hooks"] as Record<string, unknown> | undefined;
56
+ if (!hooks || !Array.isArray(hooks["UserPromptSubmit"])) return false;
57
+
58
+ const before = hooks["UserPromptSubmit"] as unknown[];
59
+ const after = before.filter((entry) => !isAgentlogHookEntry(entry));
60
+
61
+ if (after.length === before.length) return false; // nothing removed
62
+
63
+ hooks["UserPromptSubmit"] = after;
64
+ writeClaudeSettings(settings);
65
+ return true;
66
+ }
67
+
68
+ export function registerHook(): void {
69
+ // Ensure ~/.claude exists
70
+ mkdirSync(join(homedir(), ".claude"), { recursive: true });
71
+
72
+ let settings: Record<string, unknown> = readClaudeSettings() ?? {};
73
+
74
+ // Ensure hooks.UserPromptSubmit array exists
75
+ if (typeof settings["hooks"] !== "object" || settings["hooks"] === null) {
76
+ settings["hooks"] = {};
77
+ }
78
+ const hooks = settings["hooks"] as Record<string, unknown>;
79
+
80
+ if (!Array.isArray(hooks["UserPromptSubmit"])) {
81
+ hooks["UserPromptSubmit"] = [];
82
+ }
83
+ const upsArr = hooks["UserPromptSubmit"] as unknown[];
84
+
85
+ // Idempotent: only add if not already registered
86
+ if (!upsArr.some(isAgentlogHookEntry)) {
87
+ upsArr.push(HOOK_ENTRY);
88
+ }
89
+
90
+ writeClaudeSettings(settings);
91
+ }
92
+
93
+ /** Returns true if the agentlog hook is registered in ~/.claude/settings.json */
94
+ export function isHookRegistered(): boolean {
95
+ const settings = readClaudeSettings();
96
+ if (!settings) return false;
97
+
98
+ const hooks = settings["hooks"] as Record<string, unknown> | undefined;
99
+ if (!hooks || !Array.isArray(hooks["UserPromptSubmit"])) return false;
100
+
101
+ return (hooks["UserPromptSubmit"] as unknown[]).some(isAgentlogHookEntry);
102
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * AgentLog CLI
4
+ *
5
+ * Commands:
6
+ * agentlog init [vault] [--plain] — configure vault and register Claude Code hook
7
+ * agentlog detect — list detected Obsidian vaults
8
+ * agentlog hook — invoked by Claude Code UserPromptSubmit hook
9
+ */
10
+
11
+ import { existsSync, readFileSync, rmSync } from "fs";
12
+ import { join, resolve } from "path";
13
+ import { homedir } from "os";
14
+ import { spawnSync } from "child_process";
15
+ import { saveConfig, loadConfig, expandHome, configPath, configDir } from "./config.js";
16
+ import { detectVaults, detectCli } from "./detect.js";
17
+ import { isVersionAtLeast, MIN_CLI_VERSION, resolveCliBin, parseCliVersion } from "./obsidian-cli.js";
18
+ import { registerHook, unregisterHook, isHookRegistered, CLAUDE_SETTINGS_PATH } from "./claude-settings.js";
19
+ import * as readline from "readline";
20
+
21
+ function usage(): void {
22
+ console.log(`Usage:
23
+ agentlog init [vault] [--plain] Configure vault and register hook
24
+ agentlog detect List detected Obsidian vaults
25
+ agentlog doctor Check installation health
26
+ agentlog open Open today's Daily Note in Obsidian (CLI)
27
+ agentlog uninstall Remove hook and config
28
+ agentlog hook Run hook (called by Claude Code)
29
+
30
+ Options:
31
+ --plain Write to plain folder without Obsidian timeblock parsing
32
+ -y Skip confirmation prompt (for uninstall)
33
+ `);
34
+ }
35
+
36
+ function ask(prompt: string): Promise<string> {
37
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ return new Promise((resolve) => {
39
+ rl.question(prompt, (answer) => {
40
+ rl.close();
41
+ resolve(answer.trim());
42
+ });
43
+ });
44
+ }
45
+
46
+ async function runInit(vaultArg: string, plain: boolean): Promise<void> {
47
+ const vault = resolve(expandHome(vaultArg));
48
+
49
+ // Vault validation
50
+ if (!plain) {
51
+ const obsidianDir = join(vault, ".obsidian");
52
+ if (!existsSync(obsidianDir)) {
53
+ console.error(`
54
+ Warning: Obsidian vault not detected at: ${vault}
55
+
56
+ 1. Install Obsidian: https://obsidian.md/download
57
+ 2. Open the folder as a vault, then run:
58
+ npx agentlog init /path/to/your/vault
59
+
60
+ Or to write to a plain folder:
61
+ npx agentlog init --plain ~/notes
62
+ `);
63
+ process.exit(1);
64
+ }
65
+ } else {
66
+ // Plain mode: just check the directory exists
67
+ if (!existsSync(vault)) {
68
+ console.error(`Error: directory not found: ${vault}`);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ // Save config
74
+ saveConfig({ vault, ...(plain ? { plain: true } : {}) });
75
+ console.log(`Config saved: ${configPath()}`);
76
+ console.log(` vault: ${vault}${plain ? " (plain mode)" : ""}`);
77
+
78
+ // Probe CLI availability (informational)
79
+ if (!plain) {
80
+ const cli = detectCli();
81
+ if (cli.installed) {
82
+ console.log(` Obsidian CLI: detected (${cli.version ?? "unknown version"})`);
83
+ console.log(` Daily notes will be written via CLI when Obsidian is running.`);
84
+ } else {
85
+ console.log(` Obsidian CLI: not detected (using direct file write)`);
86
+ console.log(` For better integration, enable CLI in Obsidian 1.12+ Settings.`);
87
+ }
88
+ }
89
+
90
+ // Register hook in ~/.claude/settings.json
91
+ registerHook();
92
+ console.log(`Hook registered: ${CLAUDE_SETTINGS_PATH}`);
93
+ console.log(`
94
+ AgentLog is ready. Claude Code prompts will be logged to your Daily Note.`);
95
+ }
96
+
97
+ async function interactiveInit(plain: boolean): Promise<void> {
98
+ if (plain) {
99
+ const folder = await ask("Enter folder path for plain mode: ");
100
+ if (!folder) {
101
+ console.error("No path provided.");
102
+ process.exit(1);
103
+ }
104
+ await runInit(folder, true);
105
+ return;
106
+ }
107
+
108
+ const vaults = detectVaults();
109
+
110
+ if (vaults.length === 0) {
111
+ // F4: No vaults found
112
+ console.log("No Obsidian vaults detected.\n");
113
+ console.log("Options:");
114
+ console.log(" a) Install Obsidian: https://obsidian.md");
115
+ console.log(" brew install --cask obsidian");
116
+ console.log(" Then run: agentlog init ~/path/to/vault");
117
+ console.log("");
118
+ console.log(" b) Use plain mode (any folder):");
119
+ console.log(" agentlog init --plain ~/Documents/notes");
120
+ console.log("");
121
+
122
+ if (!process.stdin.isTTY) return;
123
+
124
+ const choice = await ask("Choose [a/b]: ");
125
+ if (choice.toLowerCase() === "b") {
126
+ const folder = await ask("Enter folder path: ");
127
+ if (folder) await runInit(folder, true);
128
+ } else {
129
+ console.log("\nVisit https://obsidian.md to install Obsidian.");
130
+ console.log("After installing, run: agentlog init ~/path/to/vault");
131
+ }
132
+ return;
133
+ }
134
+
135
+ if (vaults.length === 1) {
136
+ const v = vaults[0];
137
+ console.log(`Detected vault: ${v.path}`);
138
+ if (!process.stdin.isTTY) {
139
+ await runInit(v.path, false);
140
+ return;
141
+ }
142
+ const confirm = await ask("Use this vault? [Y/n]: ");
143
+ if (confirm === "" || confirm.toLowerCase() === "y") {
144
+ await runInit(v.path, false);
145
+ } else {
146
+ const manual = await ask("Enter vault path manually: ");
147
+ if (manual) await runInit(manual, false);
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Multiple vaults
153
+ console.log("Detected Obsidian vaults:");
154
+ vaults.forEach((v, i) => console.log(` ${i + 1}) ${v.path}`));
155
+ console.log("");
156
+
157
+ if (!process.stdin.isTTY) {
158
+ console.log(`Auto-selecting vault: ${vaults[0].path}`);
159
+ await runInit(vaults[0].path, false);
160
+ return;
161
+ }
162
+
163
+ const choice = await ask("Select vault [1]: ");
164
+ const idx = choice === "" ? 0 : parseInt(choice, 10) - 1;
165
+ const selected = vaults[idx] ?? vaults[0];
166
+ await runInit(selected.path, false);
167
+ }
168
+
169
+ async function cmdInit(args: string[]): Promise<void> {
170
+ const plain = args.includes("--plain");
171
+ const filteredArgs = args.filter((a) => a !== "--plain");
172
+ const vaultArg = filteredArgs[0] ?? "";
173
+
174
+ if (!vaultArg) {
175
+ await interactiveInit(plain);
176
+ return;
177
+ }
178
+
179
+ await runInit(vaultArg, plain);
180
+ }
181
+
182
+ /** agentlog detect — list detected Obsidian vaults */
183
+ async function cmdDetect(): Promise<void> {
184
+ const vaults = detectVaults();
185
+ if (vaults.length === 0) {
186
+ console.log("No Obsidian vaults detected.");
187
+ console.log("\nOptions:");
188
+ console.log(" Install Obsidian: https://obsidian.md");
189
+ console.log(" Or use plain mode: agentlog init --plain ~/path/to/folder");
190
+ return;
191
+ }
192
+ console.log("Detected Obsidian vaults:");
193
+ vaults.forEach((v, i) => {
194
+ console.log(` ${i + 1}) ${v.path}`);
195
+ });
196
+
197
+ // CLI detection
198
+ const cli = detectCli();
199
+ console.log("");
200
+ if (cli.installed) {
201
+ console.log(`Obsidian CLI: ${cli.binPath} (${cli.version ?? "version unknown"})`);
202
+ } else {
203
+ console.log("Obsidian CLI: not detected");
204
+ console.log(" Enable in Obsidian 1.12+ Settings > General > Command line interface");
205
+ }
206
+ }
207
+
208
+ async function cmdUninstall(args: string[]): Promise<void> {
209
+ const skipConfirm = args.includes("-y");
210
+ const cfgDir = configDir();
211
+
212
+ if (!skipConfirm && process.stdin.isTTY) {
213
+ const answer = await ask("Remove AgentLog hook and config? [y/N]: ");
214
+ if (answer.toLowerCase() !== "y") {
215
+ console.log("Aborted.");
216
+ return;
217
+ }
218
+ }
219
+
220
+ // Remove hook from ~/.claude/settings.json
221
+ const hookRemoved = unregisterHook();
222
+ if (hookRemoved) {
223
+ console.log(`Hook removed: ${CLAUDE_SETTINGS_PATH}`);
224
+ } else {
225
+ console.log(`Hook not found (already removed or never registered)`);
226
+ }
227
+
228
+ // Remove config directory
229
+ if (existsSync(cfgDir)) {
230
+ rmSync(cfgDir, { recursive: true, force: true });
231
+ console.log(`Config removed: ${cfgDir}`);
232
+ } else {
233
+ console.log(`Config not found (already removed)`);
234
+ }
235
+
236
+ console.log("\nAgentLog uninstalled.");
237
+ }
238
+
239
+ /** agentlog doctor — check installation health */
240
+ async function cmdDoctor(): Promise<void> {
241
+ // Show agentlog version
242
+ try {
243
+ const pkgPath = join(import.meta.dir ?? new URL(".", import.meta.url).pathname, "..", "package.json");
244
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
245
+ if (pkg.version) console.log(`agentlog v${pkg.version}\n`);
246
+ } catch {
247
+ // version display is best-effort
248
+ }
249
+
250
+ let allOk = true;
251
+
252
+ function check(label: string, ok: boolean, detail: string, hint?: string, warnOnly?: boolean): void {
253
+ const icon = ok ? "✅" : warnOnly ? "⚠️" : "❌";
254
+ const suffix = !ok && hint ? ` → ${hint}` : "";
255
+ console.log(`${icon} ${label.padEnd(10)} ${detail}${suffix}`);
256
+ if (!ok && !warnOnly) allOk = false;
257
+ }
258
+
259
+ // 1. Binary in PATH
260
+ const which = spawnSync("which", ["agentlog"], { encoding: "utf-8" });
261
+ const binPath = which.status === 0 ? which.stdout.trim() : "";
262
+ check("binary", !!binPath, binPath || "not found in PATH", "run: npm install -g agentlog");
263
+
264
+ // 2. Vault (covers both config presence and vault validity)
265
+ const config = loadConfig();
266
+ if (!config) {
267
+ check("vault", false, "not configured", "run: agentlog init ~/path/to/vault");
268
+ } else if (config.plain) {
269
+ const vaultOk = existsSync(config.vault);
270
+ check(
271
+ "vault",
272
+ vaultOk,
273
+ vaultOk ? `${config.vault} (plain mode)` : `${config.vault} — directory not found`,
274
+ vaultOk ? undefined : "run: agentlog init ~/new/path"
275
+ );
276
+ } else {
277
+ const vaultOk = existsSync(join(config.vault, ".obsidian"));
278
+ check(
279
+ "vault",
280
+ vaultOk,
281
+ vaultOk ? config.vault : `${config.vault} — .obsidian not found`,
282
+ vaultOk ? undefined : "open this folder in Obsidian, or run: agentlog init ~/new/vault"
283
+ );
284
+ }
285
+
286
+ // 3. Obsidian app installed (macOS only, skip for plain mode)
287
+ if (process.platform === "darwin" && (!config || !config.plain)) {
288
+ const obsidianPaths = [
289
+ "/Applications/Obsidian.app",
290
+ join(homedir(), "Applications", "Obsidian.app"),
291
+ ];
292
+ const foundPath = obsidianPaths.find((p) => existsSync(p));
293
+ check(
294
+ "obsidian",
295
+ !!foundPath,
296
+ foundPath ?? "not installed",
297
+ "brew install --cask obsidian or https://obsidian.md/download"
298
+ );
299
+ }
300
+
301
+ // 5. Obsidian CLI checks (warn-only, skip for plain mode)
302
+ if (!config || !config.plain) {
303
+ const cliBinPath = resolveCliBin();
304
+ check(
305
+ "cli",
306
+ !!cliBinPath,
307
+ cliBinPath || "not found",
308
+ "Enable CLI in Obsidian Settings > General, then register in PATH (or set OBSIDIAN_BIN)",
309
+ true
310
+ );
311
+
312
+ if (cliBinPath) {
313
+ // 5a. CLI version + minimum version check
314
+ const cliVer = spawnSync(cliBinPath, ["version"], { encoding: "utf-8", timeout: 3000 });
315
+ // stdout may contain warning lines before the version; take the last non-empty line
316
+ const version = cliVer.status === 0 ? (parseCliVersion(cliVer.stdout ?? "") ?? "") : "";
317
+ if (version) {
318
+ const meetsMin = isVersionAtLeast(version, MIN_CLI_VERSION);
319
+ check(
320
+ "cli-ver",
321
+ meetsMin,
322
+ meetsMin ? version : `${version} (requires ${MIN_CLI_VERSION}+)`,
323
+ meetsMin ? undefined : `Update Obsidian: https://help.obsidian.md/updates`,
324
+ true
325
+ );
326
+ } else {
327
+ check("cli-ver", false, "could not determine version", "Ensure Obsidian app is running", true);
328
+ }
329
+
330
+ // 5b. CLI responsive (app running + can communicate)
331
+ const cliProbe = spawnSync(cliBinPath, ["daily:path"], { encoding: "utf-8", timeout: 3000 });
332
+ check(
333
+ "cli-app",
334
+ cliProbe.status === 0,
335
+ cliProbe.status === 0 ? "responsive" : "app not responding",
336
+ "Start Obsidian app, or check CLI settings",
337
+ true
338
+ );
339
+
340
+ // 5c. Daily note status (only when CLI is responsive)
341
+ if (cliProbe.status === 0) {
342
+ const dailyRead = spawnSync(cliBinPath, ["daily:read"], { encoding: "utf-8", timeout: 3000 });
343
+ const noteExists = dailyRead.status === 0 && (dailyRead.stdout ?? "").trim().length > 0;
344
+ check(
345
+ "daily",
346
+ noteExists,
347
+ noteExists ? "today's note exists" : "today's note not found (will be created on first log)",
348
+ undefined,
349
+ true
350
+ );
351
+ }
352
+ }
353
+ }
354
+
355
+ // 6. Hook registered
356
+ const hookOk = isHookRegistered();
357
+ check(
358
+ "hook",
359
+ hookOk,
360
+ hookOk ? CLAUDE_SETTINGS_PATH : "not registered",
361
+ "run: agentlog init to re-register"
362
+ );
363
+
364
+ console.log("");
365
+ if (allOk) {
366
+ console.log("All checks passed.");
367
+ } else {
368
+ console.log("Some checks failed. Fix the issues above and re-run: agentlog doctor");
369
+ process.exit(1);
370
+ }
371
+ }
372
+
373
+ /** agentlog open — open today's Daily Note in Obsidian via CLI */
374
+ async function cmdOpen(): Promise<void> {
375
+ const proc = spawnSync("obsidian", ["daily"], {
376
+ encoding: "utf-8",
377
+ timeout: 5000,
378
+ });
379
+ if (proc.status === 0) {
380
+ console.log("Opened today's Daily Note in Obsidian.");
381
+ } else {
382
+ console.error("Failed to open. Is Obsidian running with CLI enabled?");
383
+ console.error(" Enable CLI in Obsidian 1.12+ Settings > General > Command line interface");
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ async function cmdHook(): Promise<void> {
389
+ // Dynamically import hook to avoid loading it unless needed
390
+ await import("./hook.js");
391
+ }
392
+
393
+ // --- Main dispatch ---
394
+
395
+ const [, , command, ...rest] = process.argv;
396
+
397
+ switch (command) {
398
+ case "init":
399
+ await cmdInit(rest);
400
+ break;
401
+ case "detect":
402
+ await cmdDetect();
403
+ break;
404
+ case "doctor":
405
+ await cmdDoctor();
406
+ break;
407
+ case "open":
408
+ await cmdOpen();
409
+ break;
410
+ case "uninstall":
411
+ await cmdUninstall(rest);
412
+ break;
413
+ case "hook":
414
+ await cmdHook();
415
+ break;
416
+ default:
417
+ usage();
418
+ process.exit(command ? 1 : 0);
419
+ }
package/src/config.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import type { AgentLogConfig } from "./types.js";
5
+
6
+ export function configDir(): string {
7
+ return process.env.AGENTLOG_CONFIG_DIR ?? join(homedir(), ".agentlog");
8
+ }
9
+
10
+ export function configPath(): string {
11
+ return join(configDir(), "config.json");
12
+ }
13
+
14
+ export function loadConfig(): AgentLogConfig | null {
15
+ const cfgPath = configPath();
16
+ if (!existsSync(cfgPath)) return null;
17
+ try {
18
+ const raw = readFileSync(cfgPath, "utf-8");
19
+ return JSON.parse(raw) as AgentLogConfig;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function saveConfig(config: AgentLogConfig): void {
26
+ const cfgDir = configDir();
27
+ mkdirSync(cfgDir, { recursive: true });
28
+ // Expand ~ in vault path before saving
29
+ const normalized: AgentLogConfig = {
30
+ ...config,
31
+ vault: expandHome(config.vault),
32
+ };
33
+ writeFileSync(configPath(), JSON.stringify(normalized, null, 2), "utf-8");
34
+ }
35
+
36
+ export function expandHome(p: string): string {
37
+ if (p.startsWith("~/")) {
38
+ return join(homedir(), p.slice(2));
39
+ }
40
+ return p;
41
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Obsidian vault auto-detection for agentlog init.
3
+ * Reads macOS Obsidian app registry and scans common paths.
4
+ * Also detects Obsidian CLI (1.12+) availability.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { spawnSync } from "child_process";
11
+ import { resolveCliBin, parseCliVersion } from "./obsidian-cli.js";
12
+
13
+ export interface DetectedVault {
14
+ path: string;
15
+ source: "obsidian-registry" | "common-path";
16
+ }
17
+
18
+ /**
19
+ * Detects Obsidian vaults on the current system.
20
+ * macOS: reads ~/Library/Application Support/Obsidian/obsidian.json
21
+ * All platforms: checks common paths
22
+ */
23
+ export function detectVaults(): DetectedVault[] {
24
+ const found: DetectedVault[] = [];
25
+ const seen = new Set<string>();
26
+
27
+ // macOS: Obsidian registry
28
+ if (process.platform === "darwin") {
29
+ const registryPath = join(
30
+ homedir(),
31
+ "Library/Application Support/Obsidian/obsidian.json"
32
+ );
33
+ if (existsSync(registryPath)) {
34
+ try {
35
+ const raw = readFileSync(registryPath, "utf-8");
36
+ const data = JSON.parse(raw) as { vaults?: Record<string, { path: string }> };
37
+ for (const vault of Object.values(data.vaults ?? {})) {
38
+ const vaultPath = vault.path;
39
+ if (vaultPath && existsSync(vaultPath) && !seen.has(vaultPath)) {
40
+ seen.add(vaultPath);
41
+ found.push({ path: vaultPath, source: "obsidian-registry" });
42
+ }
43
+ }
44
+ } catch {
45
+ // ignore parse errors
46
+ }
47
+ }
48
+ }
49
+
50
+ // Common paths fallback
51
+ const home = homedir();
52
+ const commonPaths = [
53
+ join(home, "Obsidian"),
54
+ join(home, "Documents", "Obsidian"),
55
+ join(home, "Notes"),
56
+ join(home, "Documents", "Notes"),
57
+ ];
58
+ for (const p of commonPaths) {
59
+ if (existsSync(join(p, ".obsidian")) && !seen.has(p)) {
60
+ seen.add(p);
61
+ found.push({ path: p, source: "common-path" });
62
+ }
63
+ }
64
+
65
+ return found;
66
+ }
67
+
68
+ export interface CliDetection {
69
+ installed: boolean;
70
+ binPath: string | null;
71
+ version: string | null;
72
+ }
73
+
74
+ /** Detect Obsidian CLI availability. Lightweight — no app-running check. */
75
+ export function detectCli(): CliDetection {
76
+ const binPath = resolveCliBin();
77
+ if (!binPath) {
78
+ return { installed: false, binPath: null, version: null };
79
+ }
80
+
81
+ const ver = spawnSync(binPath, ["version"], {
82
+ encoding: "utf-8",
83
+ timeout: 3000,
84
+ });
85
+ const version = ver.status === 0 ? parseCliVersion(ver.stdout ?? "") : null;
86
+
87
+ return { installed: true, binPath, version };
88
+ }