fathom-mcp 0.4.5 → 0.4.6
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/package.json +1 -1
- package/scripts/fathom-sessionstart.sh +72 -0
- package/src/cli.js +410 -83
- package/src/config.js +10 -0
- package/src/index.js +147 -47
- package/src/server-client.js +57 -4
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Fathom SessionStart hook — version check on session startup.
|
|
3
|
+
# Compares local .fathom/version against npm registry and injects update notice.
|
|
4
|
+
#
|
|
5
|
+
# Output: JSON with hookSpecificOutput.additionalContext (update notice or empty).
|
|
6
|
+
# Graceful failure: if npm unreachable or version file missing, skip silently.
|
|
7
|
+
|
|
8
|
+
set -o pipefail
|
|
9
|
+
|
|
10
|
+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
TOAST="$HOOK_DIR/hook-toast.sh"
|
|
12
|
+
|
|
13
|
+
# Consume stdin (SessionStart sends JSON we don't need)
|
|
14
|
+
cat > /dev/null
|
|
15
|
+
|
|
16
|
+
# Walk up to find .fathom.json
|
|
17
|
+
find_config() {
|
|
18
|
+
local dir="$PWD"
|
|
19
|
+
while [ "$dir" != "/" ]; do
|
|
20
|
+
if [ -f "$dir/.fathom.json" ]; then
|
|
21
|
+
echo "$dir"
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
dir="$(dirname "$dir")"
|
|
25
|
+
done
|
|
26
|
+
return 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
PROJECT_DIR=$(find_config 2>/dev/null) || exit 0
|
|
30
|
+
|
|
31
|
+
VERSION_FILE="$PROJECT_DIR/.fathom/version"
|
|
32
|
+
|
|
33
|
+
# If no version file, skip silently — init hasn't been run with this version yet
|
|
34
|
+
if [ ! -f "$VERSION_FILE" ]; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
LOCAL_VERSION=$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
39
|
+
if [ -z "$LOCAL_VERSION" ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Check npm registry for latest version (2s timeout)
|
|
44
|
+
LATEST_VERSION=$(curl -s --max-time 2 "https://registry.npmjs.org/fathom-mcp/latest" 2>/dev/null \
|
|
45
|
+
| python3 -c "import json,sys; print(json.load(sys.stdin).get('version',''))" 2>/dev/null || echo "")
|
|
46
|
+
|
|
47
|
+
# If npm check failed, skip silently
|
|
48
|
+
if [ -z "$LATEST_VERSION" ]; then
|
|
49
|
+
"$TOAST" fathom "✓ Fathom v${LOCAL_VERSION}" &>/dev/null
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Compare versions
|
|
54
|
+
if [ "$LOCAL_VERSION" != "$LATEST_VERSION" ]; then
|
|
55
|
+
# Update available — inject notice and show toast
|
|
56
|
+
"$TOAST" fathom "⬆ Fathom v${LATEST_VERSION} available" &>/dev/null
|
|
57
|
+
|
|
58
|
+
python3 -c "
|
|
59
|
+
import json
|
|
60
|
+
print(json.dumps({
|
|
61
|
+
'hookSpecificOutput': {
|
|
62
|
+
'hookEventName': 'SessionStart',
|
|
63
|
+
'additionalContext': 'Fathom update available: v${LOCAL_VERSION} → v${LATEST_VERSION}. Run: npx fathom-mcp update'
|
|
64
|
+
}
|
|
65
|
+
}))
|
|
66
|
+
"
|
|
67
|
+
else
|
|
68
|
+
# Up to date — toast only, no context injection
|
|
69
|
+
"$TOAST" fathom "✓ Fathom v${LOCAL_VERSION}" &>/dev/null
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
exit 0
|
package/src/cli.js
CHANGED
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* npx fathom-mcp — Start MCP server (stdio, for .mcp.json)
|
|
8
8
|
* npx fathom-mcp init — Interactive setup wizard
|
|
9
9
|
* npx fathom-mcp status — Check server connection + workspace status
|
|
10
|
+
* npx fathom-mcp update — Update hook scripts + version file
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import fs from "fs";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import readline from "readline";
|
|
16
|
+
import { execFileSync } from "child_process";
|
|
15
17
|
import { fileURLToPath } from "url";
|
|
16
18
|
|
|
17
|
-
import { resolveConfig, writeConfig } from "./config.js";
|
|
19
|
+
import { findConfigFile, resolveConfig, writeConfig } from "./config.js";
|
|
18
20
|
import { createClient } from "./server-client.js";
|
|
19
21
|
|
|
20
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -105,6 +107,90 @@ function copyScripts(targetDir) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// --- Headless agent integration ----------------------------------------------
|
|
111
|
+
|
|
112
|
+
const HEADLESS_CMDS = {
|
|
113
|
+
"claude-code": (prompt) => ["claude", "-p", prompt],
|
|
114
|
+
"codex": (prompt) => ["codex", "exec", prompt],
|
|
115
|
+
"gemini": (prompt) => ["gemini", prompt],
|
|
116
|
+
"opencode": (prompt) => ["opencode", "run", prompt],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function buildIntegrationPrompt(blob) {
|
|
120
|
+
return [
|
|
121
|
+
"The following instructions were generated by fathom-mcp init for this project.",
|
|
122
|
+
"Add them to the file where you store persistent behavioral instructions",
|
|
123
|
+
"(e.g. CLAUDE.md for Claude Code). If the file exists, read it first and",
|
|
124
|
+
"integrate the new section without removing existing content. If a section",
|
|
125
|
+
"with the same heading already exists, replace it. If no instructions file",
|
|
126
|
+
"exists yet, create one.",
|
|
127
|
+
"",
|
|
128
|
+
"--- INSTRUCTIONS ---",
|
|
129
|
+
blob,
|
|
130
|
+
"--- END ---",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if fathom-server is available on PATH.
|
|
136
|
+
* Returns "installed" or "not-found".
|
|
137
|
+
*/
|
|
138
|
+
function detectFathomServer() {
|
|
139
|
+
try {
|
|
140
|
+
execFileSync("which", ["fathom-server"], { stdio: "pipe" });
|
|
141
|
+
return "installed";
|
|
142
|
+
} catch {
|
|
143
|
+
return "not-found";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runAgentHeadless(agentKey, prompt) {
|
|
148
|
+
const cmdBuilder = HEADLESS_CMDS[agentKey];
|
|
149
|
+
if (!cmdBuilder) return null;
|
|
150
|
+
const [cmd, ...args] = cmdBuilder(prompt);
|
|
151
|
+
try {
|
|
152
|
+
const result = execFileSync(cmd, args, {
|
|
153
|
+
cwd: process.cwd(),
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
156
|
+
timeout: 60000,
|
|
157
|
+
});
|
|
158
|
+
return result;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- CLI flag parsing --------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
function parseFlags(argv) {
|
|
167
|
+
const flags = {
|
|
168
|
+
nonInteractive: false,
|
|
169
|
+
apiKey: null,
|
|
170
|
+
server: null,
|
|
171
|
+
workspace: null,
|
|
172
|
+
};
|
|
173
|
+
for (let i = 0; i < argv.length; i++) {
|
|
174
|
+
if (argv[i] === "-y" || argv[i] === "--yes") {
|
|
175
|
+
flags.nonInteractive = true;
|
|
176
|
+
} else if (argv[i] === "--api-key" && argv[i + 1]) {
|
|
177
|
+
flags.apiKey = argv[i + 1];
|
|
178
|
+
i++;
|
|
179
|
+
} else if (argv[i] === "--server" && argv[i + 1]) {
|
|
180
|
+
flags.server = argv[i + 1];
|
|
181
|
+
i++;
|
|
182
|
+
} else if (argv[i] === "--workspace" && argv[i + 1]) {
|
|
183
|
+
flags.workspace = argv[i + 1];
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Check environment variables as fallback
|
|
188
|
+
if (!flags.apiKey && process.env.FATHOM_API_KEY) {
|
|
189
|
+
flags.apiKey = process.env.FATHOM_API_KEY;
|
|
190
|
+
}
|
|
191
|
+
return flags;
|
|
192
|
+
}
|
|
193
|
+
|
|
108
194
|
// --- Agent registry ----------------------------------------------------------
|
|
109
195
|
|
|
110
196
|
const MCP_SERVER_ENTRY = {
|
|
@@ -208,9 +294,18 @@ export { AGENTS, writeMcpJson, writeCodexToml, writeGeminiJson, writeOpencodeJso
|
|
|
208
294
|
|
|
209
295
|
// --- Init wizard -------------------------------------------------------------
|
|
210
296
|
|
|
211
|
-
async function runInit() {
|
|
297
|
+
async function runInit(flags = {}) {
|
|
298
|
+
const {
|
|
299
|
+
nonInteractive = false,
|
|
300
|
+
apiKey: flagApiKey = null,
|
|
301
|
+
server: flagServer = null,
|
|
302
|
+
workspace: flagWorkspace = null,
|
|
303
|
+
} = flags;
|
|
212
304
|
const cwd = process.cwd();
|
|
213
|
-
|
|
305
|
+
|
|
306
|
+
const rl = nonInteractive
|
|
307
|
+
? null
|
|
308
|
+
: readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
214
309
|
|
|
215
310
|
console.log(`
|
|
216
311
|
▐▘ ▗ ▌
|
|
@@ -221,83 +316,152 @@ async function runInit() {
|
|
|
221
316
|
hifathom.com · fathom@myrakrusemark.com
|
|
222
317
|
`);
|
|
223
318
|
|
|
319
|
+
// Non-interactive: require API key
|
|
320
|
+
if (nonInteractive && !flagApiKey) {
|
|
321
|
+
console.error(" Error: --api-key required in non-interactive mode.");
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
224
325
|
// Check for existing config in *this* directory only (don't walk up —
|
|
225
326
|
// a parent's .fathom.json belongs to a different workspace)
|
|
226
327
|
const localConfigPath = path.join(cwd, ".fathom.json");
|
|
227
328
|
if (fs.existsSync(localConfigPath)) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.log(
|
|
232
|
-
rl
|
|
233
|
-
|
|
329
|
+
if (nonInteractive) {
|
|
330
|
+
console.log(` Overwriting existing config at: ${localConfigPath}`);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(` Found existing config at: ${localConfigPath}`);
|
|
333
|
+
const proceed = await askYesNo(rl, " Overwrite?", false);
|
|
334
|
+
if (!proceed) {
|
|
335
|
+
console.log(" Aborted.");
|
|
336
|
+
rl.close();
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
234
339
|
}
|
|
235
340
|
}
|
|
236
341
|
|
|
237
342
|
// 1. Workspace name
|
|
238
343
|
const defaultName = path.basename(cwd);
|
|
239
|
-
const workspace =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const vault = await ask(rl, " Vault subdirectory", "vault");
|
|
344
|
+
const workspace = nonInteractive
|
|
345
|
+
? (flagWorkspace || defaultName)
|
|
346
|
+
: await ask(rl, " Workspace name", defaultName);
|
|
243
347
|
|
|
244
|
-
//
|
|
245
|
-
const description = await ask(rl, " Workspace description (optional)", "");
|
|
348
|
+
// 2. Description (optional)
|
|
349
|
+
const description = nonInteractive ? "" : await ask(rl, " Workspace description (optional)", "");
|
|
246
350
|
|
|
247
351
|
// 4. Agent selection — auto-detect and let user choose
|
|
248
352
|
const agentKeys = Object.keys(AGENTS);
|
|
249
353
|
const detected = agentKeys.filter((key) => AGENTS[key].detect(cwd));
|
|
250
354
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
355
|
+
let selectedAgents;
|
|
356
|
+
if (nonInteractive) {
|
|
357
|
+
selectedAgents = detected.length > 0 ? [detected[0]] : ["claude-code"];
|
|
358
|
+
console.log(` Agent: ${AGENTS[selectedAgents[0]].name} (auto-detected)`);
|
|
359
|
+
} else {
|
|
360
|
+
console.log("\n Detected agents:");
|
|
361
|
+
for (const key of agentKeys) {
|
|
362
|
+
const agent = AGENTS[key];
|
|
363
|
+
const isDetected = detected.includes(key);
|
|
364
|
+
const mark = isDetected ? "✓" : " ";
|
|
365
|
+
const markers = { "claude-code": ".claude/", "codex": ".codex/", "gemini": ".gemini/", "opencode": "opencode.json" };
|
|
366
|
+
const hint = isDetected ? ` (${markers[key] || key} found)` : "";
|
|
367
|
+
console.log(` ${mark} ${agent.name}${hint}`);
|
|
368
|
+
}
|
|
260
369
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
370
|
+
console.log("\n Configure for which agents?");
|
|
371
|
+
agentKeys.forEach((key, i) => {
|
|
372
|
+
const agent = AGENTS[key];
|
|
373
|
+
const mark = detected.includes(key) ? " ✓" : "";
|
|
374
|
+
console.log(` ${i + 1}. ${agent.name}${mark}`);
|
|
375
|
+
});
|
|
267
376
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
377
|
+
const defaultSelection = detected.length > 0
|
|
378
|
+
? detected.map((key) => agentKeys.indexOf(key) + 1).join(",")
|
|
379
|
+
: "1";
|
|
380
|
+
const selectionStr = await ask(rl, "\n Enter numbers, comma-separated", defaultSelection);
|
|
272
381
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
382
|
+
const selectedIndices = selectionStr
|
|
383
|
+
.split(",")
|
|
384
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
385
|
+
.filter((n) => n >= 1 && n <= agentKeys.length);
|
|
386
|
+
selectedAgents = [...new Set(selectedIndices.map((i) => agentKeys[i - 1]))];
|
|
278
387
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
388
|
+
if (selectedAgents.length === 0) {
|
|
389
|
+
console.log(" No agents selected. Defaulting to Claude Code.");
|
|
390
|
+
selectedAgents.push("claude-code");
|
|
391
|
+
}
|
|
282
392
|
}
|
|
283
393
|
|
|
284
394
|
// 5. Server URL
|
|
285
|
-
const serverUrl =
|
|
395
|
+
const serverUrl = nonInteractive
|
|
396
|
+
? (flagServer || "http://localhost:4243")
|
|
397
|
+
: await ask(rl, "\n Fathom server URL", "http://localhost:4243");
|
|
286
398
|
|
|
287
399
|
// 6. API key
|
|
288
|
-
const apiKey = await ask(rl, " API key (from dashboard or server first-run output)", "");
|
|
400
|
+
const apiKey = flagApiKey || (nonInteractive ? "" : await ask(rl, " API key (from dashboard or server first-run output)", ""));
|
|
401
|
+
|
|
402
|
+
// 7. Server probe — check reachability early
|
|
403
|
+
const regClient = createClient({ server: serverUrl, apiKey, workspace });
|
|
404
|
+
const serverReachable = serverUrl ? await regClient.healthCheck() : false;
|
|
405
|
+
const serverOnPath = detectFathomServer();
|
|
289
406
|
|
|
290
|
-
|
|
407
|
+
if (!serverReachable) {
|
|
408
|
+
console.log(`\n ⚠ Fathom server not reachable at ${serverUrl}\n`);
|
|
409
|
+
if (serverOnPath === "installed") {
|
|
410
|
+
console.log(" Start it: fathom-server");
|
|
411
|
+
} else {
|
|
412
|
+
console.log(" Install & start it:");
|
|
413
|
+
console.log(" pip install fathom-server && fathom-server");
|
|
414
|
+
console.log(" # or: docker run -p 4243:4243 ghcr.io/myra/fathom-server");
|
|
415
|
+
}
|
|
416
|
+
console.log("\n Without the server, only \"local\" and \"none\" vault modes are available.");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 8. Vault mode selection
|
|
420
|
+
let vaultMode;
|
|
421
|
+
if (nonInteractive) {
|
|
422
|
+
vaultMode = serverReachable ? "hosted" : "local";
|
|
423
|
+
console.log(` Vault mode: ${vaultMode} (auto-selected)`);
|
|
424
|
+
} else {
|
|
425
|
+
if (serverReachable) {
|
|
426
|
+
console.log("\n Vault mode:");
|
|
427
|
+
console.log(" 1. Hosted (default) — vault stored on server, accessible everywhere");
|
|
428
|
+
console.log(" 2. Synced — local vault + server mirror (local is source of truth)");
|
|
429
|
+
console.log(" 3. Local — vault on disk only, not visible to server");
|
|
430
|
+
console.log(" 4. None — no vault, coordination features only");
|
|
431
|
+
const modeChoice = await ask(rl, "\n Select mode", "1");
|
|
432
|
+
const modeMap = { "1": "hosted", "2": "synced", "3": "local", "4": "none" };
|
|
433
|
+
vaultMode = modeMap[modeChoice] || "hosted";
|
|
434
|
+
} else {
|
|
435
|
+
console.log("\n Vault mode (server not available — hosted/synced require server):");
|
|
436
|
+
console.log(" 1. Local (default) — vault on disk only");
|
|
437
|
+
console.log(" 2. None — no vault, coordination features only");
|
|
438
|
+
const modeChoice = await ask(rl, "\n Select mode", "1");
|
|
439
|
+
vaultMode = modeChoice === "2" ? "none" : "local";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Only ask about vault subdirectory if mode needs a local dir
|
|
444
|
+
const needsLocalVault = vaultMode === "synced" || vaultMode === "local";
|
|
445
|
+
const vault = needsLocalVault
|
|
446
|
+
? (nonInteractive ? "vault" : await ask(rl, " Vault subdirectory", "vault"))
|
|
447
|
+
: "vault";
|
|
448
|
+
|
|
449
|
+
// 9. Hooks — only ask if Claude Code is selected
|
|
291
450
|
const hasClaude = selectedAgents.includes("claude-code");
|
|
292
451
|
let enableRecallHook = false;
|
|
293
452
|
let enablePrecompactHook = false;
|
|
294
453
|
if (hasClaude) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
454
|
+
if (nonInteractive) {
|
|
455
|
+
enableRecallHook = true;
|
|
456
|
+
enablePrecompactHook = true;
|
|
457
|
+
} else {
|
|
458
|
+
console.log();
|
|
459
|
+
enableRecallHook = await askYesNo(rl, " Enable vault recall on every message (UserPromptSubmit)?", true);
|
|
460
|
+
enablePrecompactHook = await askYesNo(rl, " Enable PreCompact vault snapshot hook?", true);
|
|
461
|
+
}
|
|
298
462
|
}
|
|
299
463
|
|
|
300
|
-
rl
|
|
464
|
+
rl?.close();
|
|
301
465
|
|
|
302
466
|
// --- Write files ---
|
|
303
467
|
|
|
@@ -306,6 +470,7 @@ async function runInit() {
|
|
|
306
470
|
// .fathom.json
|
|
307
471
|
const configData = {
|
|
308
472
|
workspace,
|
|
473
|
+
vaultMode,
|
|
309
474
|
vault,
|
|
310
475
|
server: serverUrl,
|
|
311
476
|
apiKey,
|
|
@@ -326,13 +491,23 @@ async function runInit() {
|
|
|
326
491
|
console.log(` ✓ .fathom/scripts/ (${copiedScripts.length} scripts)`);
|
|
327
492
|
}
|
|
328
493
|
|
|
329
|
-
//
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
494
|
+
// .fathom/version
|
|
495
|
+
const pkgJsonPath = path.join(__dirname, "..", "package.json");
|
|
496
|
+
const pkg = readJsonFile(pkgJsonPath);
|
|
497
|
+
const packageVersion = pkg?.version || "unknown";
|
|
498
|
+
const fathomDir = path.join(cwd, ".fathom");
|
|
499
|
+
fs.writeFileSync(path.join(fathomDir, "version"), packageVersion + "\n");
|
|
500
|
+
console.log(` ✓ .fathom/version (${packageVersion})`);
|
|
501
|
+
|
|
502
|
+
// vault/ directory — only create for synced/local modes
|
|
503
|
+
if (needsLocalVault) {
|
|
504
|
+
const vaultDir = path.join(cwd, vault);
|
|
505
|
+
if (!fs.existsSync(vaultDir)) {
|
|
506
|
+
fs.mkdirSync(vaultDir, { recursive: true });
|
|
507
|
+
console.log(` ✓ ${vault}/ (created)`);
|
|
508
|
+
} else {
|
|
509
|
+
console.log(` · ${vault}/ (already exists)`);
|
|
510
|
+
}
|
|
336
511
|
}
|
|
337
512
|
|
|
338
513
|
// fathom-agents.md — boilerplate agent instructions
|
|
@@ -357,11 +532,26 @@ async function runInit() {
|
|
|
357
532
|
}
|
|
358
533
|
|
|
359
534
|
// Claude Code hooks — only if claude-code is selected
|
|
360
|
-
if (hasClaude
|
|
535
|
+
if (hasClaude) {
|
|
361
536
|
const claudeSettingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
362
537
|
const claudeSettings = readJsonFile(claudeSettingsPath) || {};
|
|
363
538
|
|
|
364
539
|
const hooks = {};
|
|
540
|
+
|
|
541
|
+
// SessionStart — always registered (version check should always be on)
|
|
542
|
+
hooks["SessionStart"] = [
|
|
543
|
+
...(claudeSettings.hooks?.["SessionStart"] || []),
|
|
544
|
+
];
|
|
545
|
+
const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
|
|
546
|
+
const hasFathomSessionStart = hooks["SessionStart"].some((entry) =>
|
|
547
|
+
entry.hooks?.some((h) => h.command === sessionStartCmd)
|
|
548
|
+
);
|
|
549
|
+
if (!hasFathomSessionStart) {
|
|
550
|
+
hooks["SessionStart"].push({
|
|
551
|
+
hooks: [{ type: "command", command: sessionStartCmd, timeout: 10000 }],
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
365
555
|
if (enableRecallHook) {
|
|
366
556
|
hooks["UserPromptSubmit"] = [
|
|
367
557
|
...(claudeSettings.hooks?.["UserPromptSubmit"] || []),
|
|
@@ -402,41 +592,125 @@ async function runInit() {
|
|
|
402
592
|
appendToGitignore(cwd, [".fathom.json", ".fathom/scripts/"]);
|
|
403
593
|
console.log(" ✓ .gitignore");
|
|
404
594
|
|
|
405
|
-
// Register with server
|
|
406
|
-
if (
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
console.log(` ✓ Registered workspace "${workspace}" with server`);
|
|
418
|
-
} else if (regResult.error) {
|
|
419
|
-
console.log(` · Server: ${regResult.error}`);
|
|
420
|
-
}
|
|
595
|
+
// Register with server
|
|
596
|
+
if (serverReachable) {
|
|
597
|
+
const regResult = await regClient.registerWorkspace(workspace, cwd, {
|
|
598
|
+
vault,
|
|
599
|
+
description,
|
|
600
|
+
agents: selectedAgents,
|
|
601
|
+
type: selectedAgents[0] || "local",
|
|
602
|
+
});
|
|
603
|
+
if (regResult.ok) {
|
|
604
|
+
console.log(` ✓ Registered workspace "${workspace}" with server`);
|
|
605
|
+
} else if (regResult.error) {
|
|
606
|
+
console.log(` · Server registration: ${regResult.error}`);
|
|
421
607
|
}
|
|
422
608
|
}
|
|
423
609
|
|
|
424
|
-
//
|
|
610
|
+
// Context-aware next steps
|
|
425
611
|
console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
|
|
612
|
+
console.log(` Vault mode: ${vaultMode}`);
|
|
426
613
|
console.log("\n Next steps:");
|
|
427
|
-
|
|
614
|
+
|
|
615
|
+
if (!serverReachable) {
|
|
616
|
+
if (serverOnPath === "installed") {
|
|
617
|
+
console.log(" 1. Start the server: fathom-server");
|
|
618
|
+
} else {
|
|
619
|
+
console.log(" 1. Install & start the server: pip install fathom-server && fathom-server");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const stepNum = serverReachable ? 1 : 2;
|
|
624
|
+
switch (vaultMode) {
|
|
625
|
+
case "hosted":
|
|
626
|
+
console.log(` ${stepNum}. Your vault is stored on the server. Start writing!`);
|
|
627
|
+
break;
|
|
628
|
+
case "synced":
|
|
629
|
+
console.log(` ${stepNum}. Local vault syncs to server. Files in ./${vault}/ are the source of truth.`);
|
|
630
|
+
break;
|
|
631
|
+
case "local":
|
|
632
|
+
console.log(` ${stepNum}. Local vault only. Server can't search or peek into it.`);
|
|
633
|
+
break;
|
|
634
|
+
case "none":
|
|
635
|
+
console.log(` ${stepNum}. No vault configured. Rooms, messaging, and search still work.`);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
428
638
|
for (const agentKey of selectedAgents) {
|
|
429
639
|
const agent = AGENTS[agentKey];
|
|
430
640
|
console.log(` · ${agent.name}: ${agent.nextSteps}`);
|
|
431
641
|
}
|
|
642
|
+
|
|
643
|
+
// Auto-integrate agent instructions
|
|
644
|
+
const agentMdPath = path.join(cwd, ".fathom", "fathom-agents.md");
|
|
645
|
+
let instructionsBlob = "";
|
|
646
|
+
try {
|
|
647
|
+
instructionsBlob = fs.readFileSync(agentMdPath, "utf-8");
|
|
648
|
+
} catch { /* file wasn't created — use empty */ }
|
|
649
|
+
|
|
650
|
+
const primaryAgent = selectedAgents[0];
|
|
651
|
+
|
|
652
|
+
if (instructionsBlob) {
|
|
653
|
+
const prompt = buildIntegrationPrompt(instructionsBlob);
|
|
654
|
+
const cmdParts = HEADLESS_CMDS[primaryAgent]?.(prompt);
|
|
655
|
+
|
|
656
|
+
if (nonInteractive) {
|
|
657
|
+
if (cmdParts) {
|
|
658
|
+
console.log(`\n Integrating instructions via ${AGENTS[primaryAgent].name}...`);
|
|
659
|
+
const result = runAgentHeadless(primaryAgent, prompt);
|
|
660
|
+
if (result !== null) {
|
|
661
|
+
console.log(result);
|
|
662
|
+
} else {
|
|
663
|
+
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
664
|
+
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
if (cmdParts) {
|
|
671
|
+
const [cmd, ...args] = cmdParts;
|
|
672
|
+
const displayCmd = `${cmd} ${args[0]}${args.length > 1 ? " ..." : ""}`;
|
|
673
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
674
|
+
console.log("\n" + "─".repeat(60));
|
|
675
|
+
const integrate = await askYesNo(
|
|
676
|
+
rl2,
|
|
677
|
+
`\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n Proceed?`,
|
|
678
|
+
true,
|
|
679
|
+
);
|
|
680
|
+
rl2.close();
|
|
681
|
+
|
|
682
|
+
if (integrate) {
|
|
683
|
+
console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
|
|
684
|
+
const result = runAgentHeadless(primaryAgent, prompt);
|
|
685
|
+
if (result !== null) {
|
|
686
|
+
console.log(result);
|
|
687
|
+
} else {
|
|
688
|
+
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
689
|
+
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
console.log("\n No agent instructions template found — skipping integration.\n");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function printInstructionsFallback(agentMdPath, selectedAgents) {
|
|
704
|
+
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
705
|
+
const docTarget = hasNonClaude
|
|
706
|
+
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
707
|
+
: "your CLAUDE.md";
|
|
708
|
+
|
|
432
709
|
console.log(`
|
|
433
|
-
Agent instructions:
|
|
434
|
-
Some instructions are needed for your agent to use Fathom + Memento
|
|
435
|
-
effectively (memory discipline, vault conventions, cross-workspace
|
|
436
|
-
communication). Saved to: .fathom/fathom-agents.md
|
|
710
|
+
Agent instructions saved to: ${path.relative(process.cwd(), agentMdPath)}
|
|
437
711
|
|
|
438
|
-
|
|
439
|
-
|
|
712
|
+
Paste into ${docTarget}, or point your agent at the file
|
|
713
|
+
and ask it to integrate the instructions.
|
|
440
714
|
`);
|
|
441
715
|
}
|
|
442
716
|
|
|
@@ -449,6 +723,7 @@ async function runStatus() {
|
|
|
449
723
|
console.log("\n Fathom MCP Status\n");
|
|
450
724
|
console.log(` Config: ${config._configPath || "(not found — using defaults)"}`);
|
|
451
725
|
console.log(` Workspace: ${config.workspace}`);
|
|
726
|
+
console.log(` Vault mode: ${config.vaultMode}`);
|
|
452
727
|
console.log(` Vault: ${config.vault}`);
|
|
453
728
|
console.log(` Server: ${config.server}`);
|
|
454
729
|
console.log(` API Key: ${config.apiKey ? config.apiKey.slice(0, 7) + "..." + config.apiKey.slice(-4) : "(not set)"}`);
|
|
@@ -484,6 +759,44 @@ async function runStatus() {
|
|
|
484
759
|
console.log();
|
|
485
760
|
}
|
|
486
761
|
|
|
762
|
+
// --- Update command ----------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
async function runUpdate() {
|
|
765
|
+
const found = findConfigFile(process.cwd());
|
|
766
|
+
if (!found) {
|
|
767
|
+
console.error(" Error: No .fathom.json found. Run `npx fathom-mcp init` first.");
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const projectDir = found.dir;
|
|
772
|
+
const fathomDir = path.join(projectDir, ".fathom");
|
|
773
|
+
|
|
774
|
+
// Read package version from our own package.json
|
|
775
|
+
const pkgJsonPath = path.join(__dirname, "..", "package.json");
|
|
776
|
+
const pkg = readJsonFile(pkgJsonPath);
|
|
777
|
+
const packageVersion = pkg?.version || "unknown";
|
|
778
|
+
|
|
779
|
+
// Copy all scripts
|
|
780
|
+
const scriptsDir = path.join(fathomDir, "scripts");
|
|
781
|
+
const copiedScripts = copyScripts(scriptsDir);
|
|
782
|
+
|
|
783
|
+
// Write version file
|
|
784
|
+
fs.mkdirSync(fathomDir, { recursive: true });
|
|
785
|
+
fs.writeFileSync(path.join(fathomDir, "version"), packageVersion + "\n");
|
|
786
|
+
|
|
787
|
+
console.log(`\n ✓ Fathom hooks updated to v${packageVersion}\n`);
|
|
788
|
+
|
|
789
|
+
if (copiedScripts.length > 0) {
|
|
790
|
+
console.log(" Updated scripts:");
|
|
791
|
+
for (const script of copiedScripts) {
|
|
792
|
+
console.log(` ${script}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
console.log(`\n Version written to .fathom/version`);
|
|
797
|
+
console.log(" Restart your agent session to pick up changes.\n");
|
|
798
|
+
}
|
|
799
|
+
|
|
487
800
|
// --- Main --------------------------------------------------------------------
|
|
488
801
|
|
|
489
802
|
// Guard: only run CLI when this module is the entry point (not when imported by tests)
|
|
@@ -495,7 +808,8 @@ if (isMain) {
|
|
|
495
808
|
const command = process.argv[2];
|
|
496
809
|
|
|
497
810
|
if (command === "init") {
|
|
498
|
-
|
|
811
|
+
const flags = parseFlags(process.argv.slice(3));
|
|
812
|
+
runInit(flags).catch((e) => {
|
|
499
813
|
console.error(`Error: ${e.message}`);
|
|
500
814
|
process.exit(1);
|
|
501
815
|
});
|
|
@@ -504,12 +818,25 @@ if (isMain) {
|
|
|
504
818
|
console.error(`Error: ${e.message}`);
|
|
505
819
|
process.exit(1);
|
|
506
820
|
});
|
|
821
|
+
} else if (command === "update") {
|
|
822
|
+
runUpdate().catch((e) => {
|
|
823
|
+
console.error(`Error: ${e.message}`);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
});
|
|
507
826
|
} else if (!command || command === "serve") {
|
|
508
827
|
// Default: start MCP server
|
|
509
828
|
import("./index.js");
|
|
510
829
|
} else {
|
|
511
830
|
console.error(`Unknown command: ${command}`);
|
|
512
|
-
console.error(
|
|
831
|
+
console.error(`Usage: fathom-mcp [init|status|update|serve]
|
|
832
|
+
|
|
833
|
+
fathom-mcp init Interactive setup
|
|
834
|
+
fathom-mcp init -y --api-key KEY Non-interactive setup
|
|
835
|
+
fathom-mcp init -y --api-key KEY --server URL Custom server URL
|
|
836
|
+
fathom-mcp init -y --api-key KEY --workspace NAME Custom workspace name
|
|
837
|
+
fathom-mcp status Check connection status
|
|
838
|
+
fathom-mcp update Update hooks + version
|
|
839
|
+
fathom-mcp Start MCP server`);
|
|
513
840
|
process.exit(1);
|
|
514
841
|
}
|
|
515
842
|
}
|
package/src/config.js
CHANGED
|
@@ -12,9 +12,12 @@ import path from "path";
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_FILENAME = ".fathom.json";
|
|
14
14
|
|
|
15
|
+
const VALID_VAULT_MODES = new Set(["hosted", "synced", "local", "none"]);
|
|
16
|
+
|
|
15
17
|
const DEFAULTS = {
|
|
16
18
|
workspace: "",
|
|
17
19
|
vault: "vault",
|
|
20
|
+
vaultMode: "local", // hosted | synced | local | none
|
|
18
21
|
server: "http://localhost:4243",
|
|
19
22
|
apiKey: "",
|
|
20
23
|
description: "",
|
|
@@ -65,6 +68,9 @@ export function resolveConfig(startDir = process.cwd()) {
|
|
|
65
68
|
const { config } = found;
|
|
66
69
|
if (config.workspace) result.workspace = config.workspace;
|
|
67
70
|
if (config.vault) result.vault = config.vault;
|
|
71
|
+
if (config.vaultMode && VALID_VAULT_MODES.has(config.vaultMode)) {
|
|
72
|
+
result.vaultMode = config.vaultMode;
|
|
73
|
+
}
|
|
68
74
|
if (config.server) result.server = config.server;
|
|
69
75
|
if (config.apiKey) result.apiKey = config.apiKey;
|
|
70
76
|
if (config.description) result.description = config.description;
|
|
@@ -81,6 +87,9 @@ export function resolveConfig(startDir = process.cwd()) {
|
|
|
81
87
|
if (process.env.FATHOM_API_KEY) result.apiKey = process.env.FATHOM_API_KEY;
|
|
82
88
|
if (process.env.FATHOM_WORKSPACE) result.workspace = process.env.FATHOM_WORKSPACE;
|
|
83
89
|
if (process.env.FATHOM_VAULT_DIR) result.vault = process.env.FATHOM_VAULT_DIR;
|
|
90
|
+
if (process.env.FATHOM_VAULT_MODE && VALID_VAULT_MODES.has(process.env.FATHOM_VAULT_MODE)) {
|
|
91
|
+
result.vaultMode = process.env.FATHOM_VAULT_MODE;
|
|
92
|
+
}
|
|
84
93
|
|
|
85
94
|
// Derive workspace name from directory if still empty
|
|
86
95
|
if (!result.workspace) {
|
|
@@ -111,6 +120,7 @@ export function writeConfig(dir, config) {
|
|
|
111
120
|
const filePath = path.join(dir, CONFIG_FILENAME);
|
|
112
121
|
const data = {
|
|
113
122
|
workspace: config.workspace,
|
|
123
|
+
vaultMode: config.vaultMode || "local",
|
|
114
124
|
vault: config.vault || "vault",
|
|
115
125
|
server: config.server || DEFAULTS.server,
|
|
116
126
|
apiKey: config.apiKey || "",
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - server-client.js (HTTP to fathom-server — search, rooms, workspaces)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
11
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
16
|
import {
|
|
@@ -224,13 +227,14 @@ const tools = [
|
|
|
224
227
|
"to the latest message. Default: 60 minutes before the latest message. Use start to look " +
|
|
225
228
|
"further back. Example: minutes=15, start=120 returns 15 minutes of conversation starting " +
|
|
226
229
|
"2 hours before the latest message. Response includes window metadata with has_older flag " +
|
|
227
|
-
"for pseudo-pagination.",
|
|
230
|
+
"for pseudo-pagination. Automatically marks the room as read for this workspace unless mark_read=false.",
|
|
228
231
|
inputSchema: {
|
|
229
232
|
type: "object",
|
|
230
233
|
properties: {
|
|
231
234
|
room: { type: "string", description: "Room name to read from" },
|
|
232
235
|
minutes: { type: "number", description: "Window duration in minutes. Default: 60." },
|
|
233
236
|
start: { type: "number", description: "Offset in minutes from the latest message. Default: 0 (window ends at latest message). Set to 120 to look back starting 2 hours before the latest message." },
|
|
237
|
+
mark_read: { type: "boolean", description: "Set to false to peek without marking the room as read. Default: true." },
|
|
234
238
|
},
|
|
235
239
|
required: ["room"],
|
|
236
240
|
},
|
|
@@ -238,8 +242,9 @@ const tools = [
|
|
|
238
242
|
{
|
|
239
243
|
name: "fathom_room_list",
|
|
240
244
|
description:
|
|
241
|
-
"List all rooms with activity summary — message count, last activity time, last sender,
|
|
242
|
-
"Use to discover active rooms
|
|
245
|
+
"List all rooms with activity summary — message count, last activity time, last sender, " +
|
|
246
|
+
"description, and per-room unread_count for this workspace. Use to discover active rooms " +
|
|
247
|
+
"and see which have new messages.",
|
|
243
248
|
inputSchema: {
|
|
244
249
|
type: "object",
|
|
245
250
|
properties: {},
|
|
@@ -288,19 +293,39 @@ const tools = [
|
|
|
288
293
|
},
|
|
289
294
|
];
|
|
290
295
|
|
|
291
|
-
// --- Vault
|
|
296
|
+
// --- Vault routing by mode ---------------------------------------------------
|
|
292
297
|
|
|
293
298
|
/**
|
|
294
|
-
* Resolve vault
|
|
295
|
-
*
|
|
299
|
+
* Resolve vault routing for a tool call based on vault mode and workspace.
|
|
300
|
+
*
|
|
301
|
+
* Returns an object describing where to route:
|
|
302
|
+
* { local: true, vaultPath, sync? } — local disk (local/synced mode)
|
|
303
|
+
* { local: false, workspace } — server API (hosted mode / cross-workspace)
|
|
304
|
+
* { local: false, disabled: true } — vault not configured (none mode)
|
|
296
305
|
*/
|
|
297
306
|
function resolveVault(args) {
|
|
298
307
|
const ws = args.workspace;
|
|
299
|
-
|
|
300
|
-
|
|
308
|
+
const isOwnWorkspace = !ws || ws === config.workspace;
|
|
309
|
+
|
|
310
|
+
// Cross-workspace: always route through server
|
|
311
|
+
if (!isOwnWorkspace) {
|
|
312
|
+
return { local: false, workspace: ws };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Own workspace: route by vault mode
|
|
316
|
+
switch (config.vaultMode) {
|
|
317
|
+
case "hosted":
|
|
318
|
+
return { local: false, workspace: config.workspace };
|
|
319
|
+
case "synced":
|
|
320
|
+
return { local: true, vaultPath: config.vault, sync: true };
|
|
321
|
+
case "local":
|
|
322
|
+
return { local: true, vaultPath: config.vault, sync: false };
|
|
323
|
+
case "none":
|
|
324
|
+
return { local: false, disabled: true };
|
|
325
|
+
default:
|
|
326
|
+
// Fallback for existing installs without vaultMode — behave as local
|
|
327
|
+
return { local: true, vaultPath: config.vault, sync: false };
|
|
301
328
|
}
|
|
302
|
-
// Cross-workspace — delegate to server
|
|
303
|
-
return { vaultPath: null, local: false, workspace: ws };
|
|
304
329
|
}
|
|
305
330
|
|
|
306
331
|
// --- Server setup & dispatch -------------------------------------------------
|
|
@@ -317,87 +342,108 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
317
342
|
let result;
|
|
318
343
|
|
|
319
344
|
switch (name) {
|
|
320
|
-
// ---
|
|
345
|
+
// --- Vault write ---
|
|
321
346
|
case "fathom_vault_write": {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
result = { error: "
|
|
325
|
-
} else {
|
|
326
|
-
result = handleVaultWrite(args, vaultPath);
|
|
347
|
+
const v = resolveVault(args);
|
|
348
|
+
if (v.disabled) {
|
|
349
|
+
result = { error: "Vault not configured for this workspace. Use hosted or synced mode, or create a local ./vault/ directory." };
|
|
350
|
+
} else if (v.local) {
|
|
351
|
+
result = handleVaultWrite(args, v.vaultPath);
|
|
327
352
|
if (result.ok) {
|
|
328
|
-
// Fire-and-forget: notify server for access tracking
|
|
329
353
|
client.notifyAccess(args.path, args.workspace).catch(() => {});
|
|
354
|
+
if (v.sync) client.pushFile(config.workspace, args.path, args.content).catch(() => {});
|
|
330
355
|
}
|
|
356
|
+
} else {
|
|
357
|
+
result = await client.writeFile(v.workspace, args.path, args.content);
|
|
331
358
|
}
|
|
332
359
|
break;
|
|
333
360
|
}
|
|
361
|
+
|
|
362
|
+
// --- Vault append ---
|
|
334
363
|
case "fathom_vault_append": {
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
result = { error: "
|
|
338
|
-
} else {
|
|
339
|
-
result = handleVaultAppend(args, vaultPath);
|
|
364
|
+
const v = resolveVault(args);
|
|
365
|
+
if (v.disabled) {
|
|
366
|
+
result = { error: "Vault not configured for this workspace. Use hosted or synced mode, or create a local ./vault/ directory." };
|
|
367
|
+
} else if (v.local) {
|
|
368
|
+
result = handleVaultAppend(args, v.vaultPath);
|
|
340
369
|
if (result.ok) {
|
|
341
370
|
client.notifyAccess(args.path, args.workspace).catch(() => {});
|
|
371
|
+
if (v.sync) client.appendFile(config.workspace, args.path, args.content).catch(() => {});
|
|
342
372
|
}
|
|
373
|
+
} else {
|
|
374
|
+
result = await client.appendFile(v.workspace, args.path, args.content);
|
|
343
375
|
}
|
|
344
376
|
break;
|
|
345
377
|
}
|
|
378
|
+
|
|
379
|
+
// --- Vault read ---
|
|
346
380
|
case "fathom_vault_read": {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
result = handleVaultRead(args, vaultPath);
|
|
381
|
+
const v = resolveVault(args);
|
|
382
|
+
if (v.disabled) {
|
|
383
|
+
result = { error: "Vault not configured for this workspace. Use hosted or synced mode, or create a local ./vault/ directory." };
|
|
384
|
+
} else if (v.local) {
|
|
385
|
+
result = handleVaultRead(args, v.vaultPath);
|
|
353
386
|
if (!result.error) {
|
|
354
387
|
client.notifyAccess(args.path, args.workspace).catch(() => {});
|
|
355
388
|
}
|
|
389
|
+
} else {
|
|
390
|
+
result = await client.readFile(v.workspace, args.path);
|
|
356
391
|
}
|
|
357
392
|
break;
|
|
358
393
|
}
|
|
394
|
+
|
|
395
|
+
// --- Vault image (local-only for now — base64 over HTTP would be wasteful) ---
|
|
359
396
|
case "fathom_vault_image": {
|
|
360
|
-
const
|
|
361
|
-
if (!local) {
|
|
362
|
-
result = { error: "
|
|
397
|
+
const v = resolveVault(args);
|
|
398
|
+
if (!v.local) {
|
|
399
|
+
result = { error: "Image reads require local or synced vault mode." };
|
|
363
400
|
} else {
|
|
364
|
-
result = handleVaultImage(args, vaultPath);
|
|
401
|
+
result = handleVaultImage(args, v.vaultPath);
|
|
365
402
|
}
|
|
366
403
|
break;
|
|
367
404
|
}
|
|
405
|
+
|
|
406
|
+
// --- Vault write asset (local-only) ---
|
|
368
407
|
case "fathom_vault_write_asset": {
|
|
369
|
-
const
|
|
370
|
-
if (!local) {
|
|
371
|
-
result = { error: "
|
|
408
|
+
const v = resolveVault(args);
|
|
409
|
+
if (!v.local) {
|
|
410
|
+
result = { error: "Asset writes require local or synced vault mode." };
|
|
372
411
|
} else {
|
|
373
|
-
result = handleVaultWriteAsset(args, vaultPath);
|
|
412
|
+
result = handleVaultWriteAsset(args, v.vaultPath);
|
|
374
413
|
}
|
|
375
414
|
break;
|
|
376
415
|
}
|
|
377
416
|
|
|
378
|
-
// ---
|
|
417
|
+
// --- Vault list ---
|
|
379
418
|
case "fathom_vault_list": {
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
419
|
+
const v = resolveVault(args);
|
|
420
|
+
if (v.disabled) {
|
|
421
|
+
result = { error: "Vault not configured for this workspace." };
|
|
422
|
+
} else if (v.local) {
|
|
382
423
|
// Try server first for activity-enriched data, fall back to local
|
|
383
424
|
result = await client.vaultList(args.workspace);
|
|
384
425
|
if (result.error) {
|
|
385
|
-
result = handleVaultList(vaultPath);
|
|
426
|
+
result = handleVaultList(v.vaultPath);
|
|
386
427
|
}
|
|
387
428
|
} else {
|
|
388
|
-
|
|
429
|
+
// Hosted or cross-workspace: server has the files
|
|
430
|
+
result = await client.listFiles(v.workspace);
|
|
389
431
|
}
|
|
390
432
|
break;
|
|
391
433
|
}
|
|
434
|
+
|
|
435
|
+
// --- Vault folder ---
|
|
392
436
|
case "fathom_vault_folder": {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
437
|
+
const v = resolveVault(args);
|
|
438
|
+
if (v.disabled) {
|
|
439
|
+
result = { error: "Vault not configured for this workspace." };
|
|
440
|
+
} else if (v.local) {
|
|
395
441
|
result = await client.vaultFolder(args.folder, args.workspace);
|
|
396
442
|
if (result.error) {
|
|
397
|
-
result = handleVaultFolder(args, vaultPath);
|
|
443
|
+
result = handleVaultFolder(args, v.vaultPath);
|
|
398
444
|
}
|
|
399
445
|
} else {
|
|
400
|
-
result = await client.vaultFolder(args.folder,
|
|
446
|
+
result = await client.vaultFolder(args.folder, v.workspace);
|
|
401
447
|
}
|
|
402
448
|
break;
|
|
403
449
|
}
|
|
@@ -416,10 +462,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
416
462
|
result = await client.roomPost(args.room, args.message, config.workspace);
|
|
417
463
|
break;
|
|
418
464
|
case "fathom_room_read":
|
|
419
|
-
result = await client.roomRead(
|
|
465
|
+
result = await client.roomRead(
|
|
466
|
+
args.room, args.minutes, args.start,
|
|
467
|
+
args.mark_read !== false ? config.workspace : undefined,
|
|
468
|
+
args.mark_read,
|
|
469
|
+
);
|
|
420
470
|
break;
|
|
421
471
|
case "fathom_room_list":
|
|
422
|
-
result = await client.roomList();
|
|
472
|
+
result = await client.roomList(config.workspace);
|
|
423
473
|
break;
|
|
424
474
|
case "fathom_room_describe":
|
|
425
475
|
result = await client.roomDescribe(args.room, args.description);
|
|
@@ -447,6 +497,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
447
497
|
};
|
|
448
498
|
});
|
|
449
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Startup sync for synced mode — scan local vault, compare with server,
|
|
502
|
+
* and push any files the server is missing.
|
|
503
|
+
*/
|
|
504
|
+
async function startupSync() {
|
|
505
|
+
if (config.vaultMode !== "synced") return;
|
|
506
|
+
if (!fs.existsSync(config.vault)) return;
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
// Build local manifest
|
|
510
|
+
const manifest = [];
|
|
511
|
+
const walkDir = (dir) => {
|
|
512
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
513
|
+
const abs = path.join(dir, entry.name);
|
|
514
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
515
|
+
walkDir(abs);
|
|
516
|
+
} else if (entry.isFile()) {
|
|
517
|
+
const stat = fs.statSync(abs);
|
|
518
|
+
manifest.push({
|
|
519
|
+
path: path.relative(config.vault, abs),
|
|
520
|
+
mtime: stat.mtime.toISOString(),
|
|
521
|
+
size: stat.size,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
walkDir(config.vault);
|
|
527
|
+
|
|
528
|
+
if (manifest.length === 0) return;
|
|
529
|
+
|
|
530
|
+
// Ask server what it needs
|
|
531
|
+
const diff = await client.syncManifest(config.workspace, manifest);
|
|
532
|
+
if (diff.error || !diff.needed) return;
|
|
533
|
+
|
|
534
|
+
// Push needed files
|
|
535
|
+
for (const filePath of diff.needed) {
|
|
536
|
+
const abs = path.resolve(config.vault, filePath);
|
|
537
|
+
if (fs.existsSync(abs)) {
|
|
538
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
539
|
+
await client.pushFile(config.workspace, filePath, content);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
// Sync failure is non-fatal — local vault is source of truth
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
450
547
|
async function main() {
|
|
451
548
|
// Auto-register workspace with server (fire-and-forget)
|
|
452
549
|
if (config.server && config.workspace) {
|
|
@@ -457,6 +554,9 @@ async function main() {
|
|
|
457
554
|
}).catch(() => {});
|
|
458
555
|
}
|
|
459
556
|
|
|
557
|
+
// Startup sync for synced mode (fire-and-forget)
|
|
558
|
+
startupSync().catch(() => {});
|
|
559
|
+
|
|
460
560
|
const transport = new StdioServerTransport();
|
|
461
561
|
await server.connect(transport);
|
|
462
562
|
}
|
package/src/server-client.js
CHANGED
|
@@ -82,14 +82,22 @@ export function createClient(config) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async function roomRead(room, minutes, start) {
|
|
85
|
+
async function roomRead(room, minutes, start, ws, markRead) {
|
|
86
86
|
return request("GET", `/api/room/${encodeURIComponent(room)}`, {
|
|
87
|
-
params: { minutes, start },
|
|
87
|
+
params: { minutes, start, workspace: ws, mark_read: markRead },
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async function roomList() {
|
|
92
|
-
return request("GET", "/api/room/list"
|
|
91
|
+
async function roomList(ws) {
|
|
92
|
+
return request("GET", "/api/room/list", {
|
|
93
|
+
params: { workspace: ws },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function roomMarkRead(room, ws) {
|
|
98
|
+
return request("POST", `/api/room/${encodeURIComponent(room)}/read`, {
|
|
99
|
+
body: { workspace: ws },
|
|
100
|
+
});
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
async function roomDescribe(room, description) {
|
|
@@ -145,6 +153,43 @@ export function createClient(config) {
|
|
|
145
153
|
});
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
// --- Hosted vault file CRUD (server-stored files) --------------------------
|
|
157
|
+
|
|
158
|
+
async function readFile(ws, filePath) {
|
|
159
|
+
return request("GET", `/api/vault/${encodeURIComponent(ws)}/files/${filePath}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writeFile(ws, filePath, content) {
|
|
163
|
+
return request("PUT", `/api/vault/${encodeURIComponent(ws)}/files/${filePath}`, {
|
|
164
|
+
body: { content },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function appendFile(ws, filePath, content) {
|
|
169
|
+
return request("POST", `/api/vault/${encodeURIComponent(ws)}/files/${filePath}/append`, {
|
|
170
|
+
body: { content },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function deleteFile(ws, filePath) {
|
|
175
|
+
return request("DELETE", `/api/vault/${encodeURIComponent(ws)}/files/${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function listFiles(ws) {
|
|
179
|
+
return request("GET", `/api/vault/${encodeURIComponent(ws)}/files`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Push a file to server storage (used by synced mode). Same as writeFile. */
|
|
183
|
+
async function pushFile(ws, filePath, content) {
|
|
184
|
+
return writeFile(ws, filePath, content);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function syncManifest(ws, manifest) {
|
|
188
|
+
return request("POST", `/api/vault/${encodeURIComponent(ws)}/sync/manifest`, {
|
|
189
|
+
body: manifest,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
148
193
|
// --- Auth ------------------------------------------------------------------
|
|
149
194
|
|
|
150
195
|
async function getApiKey() {
|
|
@@ -169,6 +214,7 @@ export function createClient(config) {
|
|
|
169
214
|
roomPost,
|
|
170
215
|
roomRead,
|
|
171
216
|
roomList,
|
|
217
|
+
roomMarkRead,
|
|
172
218
|
roomDescribe,
|
|
173
219
|
sendToWorkspace,
|
|
174
220
|
listWorkspaces,
|
|
@@ -176,6 +222,13 @@ export function createClient(config) {
|
|
|
176
222
|
notifyAccess,
|
|
177
223
|
vaultList,
|
|
178
224
|
vaultFolder,
|
|
225
|
+
readFile,
|
|
226
|
+
writeFile,
|
|
227
|
+
appendFile,
|
|
228
|
+
deleteFile,
|
|
229
|
+
listFiles,
|
|
230
|
+
pushFile,
|
|
231
|
+
syncManifest,
|
|
179
232
|
getApiKey,
|
|
180
233
|
healthCheck,
|
|
181
234
|
};
|