@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.
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/docs/cc/hook-integration.md +125 -0
- package/docs/obsidian/06-official-cli-research.md +178 -0
- package/docs/obsidian/README.md +19 -0
- package/package.json +34 -0
- package/scripts/postinstall.mjs +27 -0
- package/src/claude-settings.ts +102 -0
- package/src/cli.ts +419 -0
- package/src/config.ts +41 -0
- package/src/detect.ts +88 -0
- package/src/hook.ts +74 -0
- package/src/note-writer.ts +255 -0
- package/src/obsidian-cli.ts +86 -0
- package/src/schema/daily-note.ts +84 -0
- package/src/schema/hook-input.ts +108 -0
- package/src/schema/pretty-prompt.ts +91 -0
- package/src/types.ts +23 -0
|
@@ -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
|
+
}
|