fathom-mcp 0.5.21 → 0.6.1
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/fathom-agents.md +7 -3
- package/package.json +2 -1
- package/scripts/vault-frontmatter-lint.js +65 -0
- package/src/cli.js +45 -230
- package/src/config.js +4 -24
- package/src/frontmatter.js +77 -0
- package/src/index.js +54 -331
- package/src/server-client.js +6 -45
- package/src/ws-connection.js +4 -6
- package/src/agents.js +0 -196
- package/src/vault-ops.js +0 -386
package/fathom-agents.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## Vault
|
|
6
6
|
|
|
7
|
-
Local files live in `{{VAULT_DIR}}/`.
|
|
7
|
+
Local files live in `{{VAULT_DIR}}/`. Read, write, and edit vault files using your native file tools (`Read`, `Write`, `Edit`, `Glob`). A PostToolUse hook validates frontmatter automatically on every write.
|
|
8
8
|
|
|
9
9
|
**Folder conventions:**
|
|
10
10
|
- `research/` — reading notes, paper annotations, deep dives
|
|
11
11
|
- `thinking/` — speculative connections, insights, one file per idea
|
|
12
12
|
- `daily/` — session heartbeats and progress logs
|
|
13
13
|
|
|
14
|
-
**Frontmatter required** on
|
|
14
|
+
**Frontmatter required** on `.md` files in the vault:
|
|
15
15
|
```yaml
|
|
16
16
|
---
|
|
17
17
|
title: My Note # required (string)
|
|
@@ -21,11 +21,15 @@ status: draft # optional: draft | published | archived
|
|
|
21
21
|
---
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
**Search** across vault files using MCP tools:
|
|
25
|
+
- `fathom_vault_search` — keyword/BM25 (fast, exact match)
|
|
26
|
+
- `fathom_vault_vsearch` — semantic/vector (conceptual similarity)
|
|
27
|
+
- `fathom_vault_query` — hybrid (most thorough, slowest)
|
|
28
|
+
|
|
24
29
|
## Cross-Workspace Communication
|
|
25
30
|
|
|
26
31
|
This workspace is part of a multi-workspace system. Other workspaces exist — you can talk to them.
|
|
27
32
|
|
|
28
|
-
- **Peek at another workspace's vault:** `fathom_vault_read path="file.md" workspace="other-ws"`
|
|
29
33
|
- **Send a direct message:** `fathom_send workspace="other-ws" message="..."`
|
|
30
34
|
- **Post to a shared room:** `fathom_room_post room="general" message="..."`
|
|
31
35
|
- **Read room history:** `fathom_room_read room="general"`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fathom-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src/",
|
|
12
12
|
"scripts/*.sh",
|
|
13
13
|
"scripts/*.py",
|
|
14
|
+
"scripts/*.js",
|
|
14
15
|
"fathom-agents.md",
|
|
15
16
|
"README.md",
|
|
16
17
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vault frontmatter lint hook for Claude Code PostToolUse (Write/Edit).
|
|
5
|
+
*
|
|
6
|
+
* Reads the tool result from stdin (JSON), checks if the file is inside a
|
|
7
|
+
* vault/ directory and ends in .md. If so, validates frontmatter.
|
|
8
|
+
* Exits 0 on pass, 1 on failure (with error message on stderr).
|
|
9
|
+
*
|
|
10
|
+
* Environment: CLAUDE_TOOL_NAME, CLAUDE_FILE_PATH are set by Claude Code hooks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { parseFrontmatter, validateFrontmatter } from "../src/frontmatter.js";
|
|
16
|
+
|
|
17
|
+
const toolName = process.env.CLAUDE_TOOL_NAME || "";
|
|
18
|
+
const filePath = process.env.CLAUDE_FILE_PATH || "";
|
|
19
|
+
|
|
20
|
+
// Only check Write and Edit tools
|
|
21
|
+
if (toolName !== "Write" && toolName !== "Edit") {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Only check .md files inside a vault/ directory
|
|
26
|
+
if (!filePath || !filePath.endsWith(".md")) {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if the file is inside a vault/ directory
|
|
31
|
+
const parts = filePath.split(path.sep);
|
|
32
|
+
if (!parts.includes("vault")) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read the file and validate
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
39
|
+
|
|
40
|
+
// Only validate if file has frontmatter
|
|
41
|
+
if (!content.startsWith("---")) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { fm } = parseFrontmatter(content);
|
|
46
|
+
if (Object.keys(fm).length === 0) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errors = validateFrontmatter(fm);
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
process.stderr.write(`Vault frontmatter validation failed for ${path.basename(filePath)}:\n`);
|
|
53
|
+
for (const err of errors) {
|
|
54
|
+
process.stderr.write(` - ${err}\n`);
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// File read errors are non-fatal for the hook
|
|
60
|
+
if (e.code !== "ENOENT") {
|
|
61
|
+
process.stderr.write(`Frontmatter lint error: ${e.message}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
process.exit(0);
|
package/src/cli.js
CHANGED
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
* npx fathom-mcp init — Interactive setup wizard
|
|
9
9
|
* npx fathom-mcp status — Check server connection + workspace status
|
|
10
10
|
* npx fathom-mcp update — Update hook scripts + version file
|
|
11
|
-
* npx fathom-mcp list — List
|
|
12
|
-
* npx fathom-mcp start [name] — Start agent by name or legacy cwd-walk
|
|
13
|
-
* npx fathom-mcp stop <name> — Stop an agent
|
|
14
|
-
* npx fathom-mcp restart <name> — Restart an agent
|
|
15
|
-
* npx fathom-mcp add [name] — Add agent to registry (reads .fathom.json defaults)
|
|
16
|
-
* npx fathom-mcp remove <name>— Remove agent from registry
|
|
17
|
-
* npx fathom-mcp config <name>— Print agent config JSON
|
|
11
|
+
* npx fathom-mcp list — List workspaces + status (from server)
|
|
18
12
|
*/
|
|
19
13
|
|
|
20
14
|
import fs from "fs";
|
|
@@ -25,17 +19,6 @@ import { fileURLToPath } from "url";
|
|
|
25
19
|
|
|
26
20
|
import { findConfigFile, resolveConfig, writeConfig } from "./config.js";
|
|
27
21
|
import { createClient } from "./server-client.js";
|
|
28
|
-
import {
|
|
29
|
-
listAgents,
|
|
30
|
-
getAgent,
|
|
31
|
-
addAgent as registryAddAgent,
|
|
32
|
-
removeAgent as registryRemoveAgent,
|
|
33
|
-
isAgentRunning,
|
|
34
|
-
startAgent,
|
|
35
|
-
stopAgent,
|
|
36
|
-
defaultCommand,
|
|
37
|
-
buildEntryFromConfig,
|
|
38
|
-
} from "./agents.js";
|
|
39
22
|
|
|
40
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
41
24
|
const SCRIPTS_DIR = path.join(__dirname, "..", "scripts");
|
|
@@ -463,7 +446,7 @@ async function runInit(flags = {}) {
|
|
|
463
446
|
|
|
464
447
|
// 8. Vault mode selection
|
|
465
448
|
let vaultMode;
|
|
466
|
-
const validVaultModes = ["
|
|
449
|
+
const validVaultModes = ["synced", "local", "none"];
|
|
467
450
|
if (nonInteractive) {
|
|
468
451
|
if (!serverReachable && flagServer) {
|
|
469
452
|
console.error(`\n Error: Server at ${serverUrl} is not reachable.`);
|
|
@@ -475,26 +458,25 @@ async function runInit(flags = {}) {
|
|
|
475
458
|
console.error(` Error: unknown vault mode "${flagVaultMode}". Valid modes: ${validVaultModes.join(", ")}`);
|
|
476
459
|
process.exit(1);
|
|
477
460
|
}
|
|
478
|
-
if (
|
|
461
|
+
if (flagVaultMode === "synced" && !serverReachable) {
|
|
479
462
|
console.error(` Error: vault mode "${flagVaultMode}" requires a reachable server.`);
|
|
480
463
|
process.exit(1);
|
|
481
464
|
}
|
|
482
465
|
vaultMode = flagVaultMode;
|
|
483
466
|
console.log(` Vault mode: ${vaultMode} (--vault-mode flag)`);
|
|
484
467
|
} else {
|
|
485
|
-
vaultMode = serverReachable ? "
|
|
468
|
+
vaultMode = serverReachable ? "synced" : "local";
|
|
486
469
|
console.log(` Vault mode: ${vaultMode} (auto-selected)`);
|
|
487
470
|
}
|
|
488
471
|
} else {
|
|
489
472
|
if (serverReachable) {
|
|
490
473
|
console.log("\n Vault mode:");
|
|
491
|
-
console.log(" 1.
|
|
492
|
-
console.log(" 2.
|
|
493
|
-
console.log(" 3.
|
|
494
|
-
console.log(" 4. None — no vault, coordination features only");
|
|
474
|
+
console.log(" 1. Synced (default) — local vault + server indexing");
|
|
475
|
+
console.log(" 2. Local — vault on disk only, not visible to server");
|
|
476
|
+
console.log(" 3. None — no vault, coordination features only");
|
|
495
477
|
const modeChoice = await ask(rl, "\n Select mode", "1");
|
|
496
|
-
const modeMap = { "1": "
|
|
497
|
-
vaultMode = modeMap[modeChoice] || "
|
|
478
|
+
const modeMap = { "1": "synced", "2": "local", "3": "none" };
|
|
479
|
+
vaultMode = modeMap[modeChoice] || "synced";
|
|
498
480
|
} else {
|
|
499
481
|
console.log("\n Vault mode (server not available — hosted/synced require server):");
|
|
500
482
|
console.log(" 1. Local (default) — vault on disk only");
|
|
@@ -594,6 +576,7 @@ async function runInit(flags = {}) {
|
|
|
594
576
|
const precompactCmd = "bash ~/.config/fathom-mcp/scripts/fathom-precompact.sh";
|
|
595
577
|
|
|
596
578
|
// Claude Code hooks
|
|
579
|
+
const lintCmd = "node ~/.config/fathom-mcp/scripts/vault-frontmatter-lint.js";
|
|
597
580
|
if (hasClaude) {
|
|
598
581
|
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
599
582
|
const settings = readJsonFile(settingsPath) || {};
|
|
@@ -601,6 +584,7 @@ async function runInit(flags = {}) {
|
|
|
601
584
|
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
602
585
|
if (enableRecallHook) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 10000) || changed;
|
|
603
586
|
if (enablePrecompactHook) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
|
|
587
|
+
changed = ensureHook(settings, "PostToolUse", lintCmd, 5000) || changed;
|
|
604
588
|
if (changed) {
|
|
605
589
|
writeJsonFile(settingsPath, settings);
|
|
606
590
|
console.log(" ✓ .claude/settings.local.json (permissions + hooks)");
|
|
@@ -657,11 +641,6 @@ async function runInit(flags = {}) {
|
|
|
657
641
|
}
|
|
658
642
|
}
|
|
659
643
|
|
|
660
|
-
// Register in CLI agent registry (for ls/start/stop)
|
|
661
|
-
const entry = buildEntryFromConfig(cwd, configData);
|
|
662
|
-
registryAddAgent(workspace, entry);
|
|
663
|
-
console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
|
|
664
|
-
|
|
665
644
|
// Context-aware next steps
|
|
666
645
|
console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
|
|
667
646
|
console.log(` Vault mode: ${vaultMode}`);
|
|
@@ -677,11 +656,8 @@ async function runInit(flags = {}) {
|
|
|
677
656
|
|
|
678
657
|
const stepNum = serverReachable ? 1 : 2;
|
|
679
658
|
switch (vaultMode) {
|
|
680
|
-
case "hosted":
|
|
681
|
-
console.log(` ${stepNum}. Your vault is stored on the server. Start writing!`);
|
|
682
|
-
break;
|
|
683
659
|
case "synced":
|
|
684
|
-
console.log(` ${stepNum}. Local vault
|
|
660
|
+
console.log(` ${stepNum}. Local vault indexed by server. Files in ./${vault}/ are the source of truth.`);
|
|
685
661
|
break;
|
|
686
662
|
case "local":
|
|
687
663
|
console.log(` ${stepNum}. Local vault only. Server can't search or peek into it.`);
|
|
@@ -897,70 +873,56 @@ async function runUpdate() {
|
|
|
897
873
|
console.log("\n Restart your agent session to pick up changes.\n");
|
|
898
874
|
}
|
|
899
875
|
|
|
900
|
-
// ---
|
|
901
|
-
|
|
902
|
-
function runStart(argv) {
|
|
903
|
-
// Check if first non-flag arg matches a registry entry
|
|
904
|
-
const firstArg = argv.find((a) => !a.startsWith("-"));
|
|
905
|
-
if (firstArg) {
|
|
906
|
-
const entry = getAgent(firstArg);
|
|
907
|
-
if (entry) {
|
|
908
|
-
const result = startAgent(firstArg, entry);
|
|
909
|
-
console.log(` ${result.message}`);
|
|
910
|
-
process.exit(result.ok ? 0 : 1);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
876
|
+
// --- List command (via server API) --------------------------------------------
|
|
914
877
|
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
const centralScript = path.join(process.env.HOME, ".config", "fathom-mcp", "scripts", "fathom-start.sh");
|
|
920
|
-
const packageScript = path.join(SCRIPTS_DIR, "fathom-start.sh");
|
|
921
|
-
const script = fs.existsSync(centralScript) ? centralScript : packageScript;
|
|
878
|
+
async function runList() {
|
|
879
|
+
const config = resolveConfig();
|
|
880
|
+
const client = createClient(config);
|
|
922
881
|
|
|
923
|
-
|
|
924
|
-
|
|
882
|
+
const isUp = await client.healthCheck();
|
|
883
|
+
if (!isUp) {
|
|
884
|
+
console.error(`\n Error: Server not reachable at ${config.server}`);
|
|
885
|
+
console.error(" Start the server or check --server URL.\n");
|
|
925
886
|
process.exit(1);
|
|
926
887
|
}
|
|
927
888
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
});
|
|
933
|
-
} catch (e) {
|
|
934
|
-
process.exit(e.status || 1);
|
|
889
|
+
const wsResult = await client.listWorkspaces();
|
|
890
|
+
if (wsResult.error) {
|
|
891
|
+
console.error(`\n Error: ${wsResult.error}\n`);
|
|
892
|
+
process.exit(1);
|
|
935
893
|
}
|
|
936
|
-
}
|
|
937
894
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
function runList() {
|
|
941
|
-
const agents = listAgents();
|
|
942
|
-
const names = Object.keys(agents);
|
|
895
|
+
const profiles = wsResult.profiles || {};
|
|
896
|
+
const names = Object.keys(profiles);
|
|
943
897
|
|
|
944
898
|
if (names.length === 0) {
|
|
945
|
-
console.log("\n No
|
|
899
|
+
console.log("\n No workspaces registered on server.\n");
|
|
946
900
|
return;
|
|
947
901
|
}
|
|
948
902
|
|
|
949
|
-
|
|
950
|
-
const cols = { name: 16, type: 13, status: 10 };
|
|
903
|
+
const cols = { name: 20, type: 10, status: 12 };
|
|
951
904
|
console.log(
|
|
952
905
|
"\n " +
|
|
953
|
-
"
|
|
906
|
+
"WORKSPACE".padEnd(cols.name) +
|
|
954
907
|
"TYPE".padEnd(cols.type) +
|
|
955
908
|
"STATUS".padEnd(cols.status) +
|
|
956
|
-
"
|
|
909
|
+
"PATH",
|
|
957
910
|
);
|
|
958
911
|
|
|
959
912
|
for (const name of names) {
|
|
960
|
-
const
|
|
961
|
-
const type =
|
|
962
|
-
|
|
963
|
-
|
|
913
|
+
const p = profiles[name];
|
|
914
|
+
const type = p.type || "local";
|
|
915
|
+
let status;
|
|
916
|
+
if (type === "human") {
|
|
917
|
+
status = "human";
|
|
918
|
+
} else if (p.process && p.connected) {
|
|
919
|
+
status = "running";
|
|
920
|
+
} else if (p.process || p.connected) {
|
|
921
|
+
status = "partial";
|
|
922
|
+
} else {
|
|
923
|
+
status = "stopped";
|
|
924
|
+
}
|
|
925
|
+
const dir = (p.path || "").replace(process.env.HOME, "~");
|
|
964
926
|
|
|
965
927
|
console.log(
|
|
966
928
|
" " +
|
|
@@ -973,134 +935,6 @@ function runList() {
|
|
|
973
935
|
console.log();
|
|
974
936
|
}
|
|
975
937
|
|
|
976
|
-
// --- Stop command ------------------------------------------------------------
|
|
977
|
-
|
|
978
|
-
function runStop(name) {
|
|
979
|
-
if (!name) {
|
|
980
|
-
console.error(" Usage: fathom-mcp stop <name>");
|
|
981
|
-
process.exit(1);
|
|
982
|
-
}
|
|
983
|
-
const entry = getAgent(name);
|
|
984
|
-
if (!entry) {
|
|
985
|
-
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
986
|
-
process.exit(1);
|
|
987
|
-
}
|
|
988
|
-
const result = stopAgent(name, entry);
|
|
989
|
-
console.log(` ${result.message}`);
|
|
990
|
-
process.exit(result.ok ? 0 : 1);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// --- Restart command ---------------------------------------------------------
|
|
994
|
-
|
|
995
|
-
function runRestart(name) {
|
|
996
|
-
if (!name) {
|
|
997
|
-
console.error(" Usage: fathom-mcp restart <name>");
|
|
998
|
-
process.exit(1);
|
|
999
|
-
}
|
|
1000
|
-
const entry = getAgent(name);
|
|
1001
|
-
if (!entry) {
|
|
1002
|
-
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
1003
|
-
process.exit(1);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const stopResult = stopAgent(name, entry);
|
|
1007
|
-
if (stopResult.ok) {
|
|
1008
|
-
console.log(` ${stopResult.message}`);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
// Brief pause between stop and start
|
|
1012
|
-
try { execFileSync("sleep", ["1"], { stdio: "pipe" }); } catch { /* */ }
|
|
1013
|
-
|
|
1014
|
-
const startResult = startAgent(name, entry);
|
|
1015
|
-
console.log(` ${startResult.message}`);
|
|
1016
|
-
process.exit(startResult.ok ? 0 : 1);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// --- Add command -------------------------------------------------------------
|
|
1020
|
-
|
|
1021
|
-
async function runAdd(argv) {
|
|
1022
|
-
const flags = parseFlags(argv);
|
|
1023
|
-
const nameArg = argv.find((a) => !a.startsWith("-"));
|
|
1024
|
-
const cwd = process.cwd();
|
|
1025
|
-
|
|
1026
|
-
// Try to read .fathom.json from cwd for defaults
|
|
1027
|
-
const found = findConfigFile(cwd);
|
|
1028
|
-
const fathomConfig = found?.config || {};
|
|
1029
|
-
const projectDir = found?.dir || cwd;
|
|
1030
|
-
|
|
1031
|
-
const defaults = buildEntryFromConfig(projectDir, fathomConfig);
|
|
1032
|
-
const defaultName = fathomConfig.workspace || path.basename(projectDir);
|
|
1033
|
-
|
|
1034
|
-
if (flags.nonInteractive) {
|
|
1035
|
-
const name = nameArg || defaultName;
|
|
1036
|
-
registryAddAgent(name, defaults);
|
|
1037
|
-
console.log(` ✓ Added agent "${name}" to registry.`);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1042
|
-
|
|
1043
|
-
const name = await ask(rl, " Agent name", nameArg || defaultName);
|
|
1044
|
-
const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
|
|
1045
|
-
const agentType = await ask(rl, " Agent type (claude-code|gemini|manual)", defaults.agentType);
|
|
1046
|
-
const command = await ask(rl, " Command", defaultCommand(agentType));
|
|
1047
|
-
const server = await ask(rl, " Server URL", defaults.server);
|
|
1048
|
-
const apiKey = await ask(rl, " API key", defaults.apiKey);
|
|
1049
|
-
const vault = await ask(rl, " Vault subdirectory", defaults.vault || "vault");
|
|
1050
|
-
const vaultMode = await ask(rl, " Vault mode (hosted|synced|local|none)", defaults.vaultMode);
|
|
1051
|
-
const description = await ask(rl, " Description", defaults.description);
|
|
1052
|
-
|
|
1053
|
-
rl.close();
|
|
1054
|
-
|
|
1055
|
-
const entry = {
|
|
1056
|
-
projectDir: path.resolve(agentProjectDir),
|
|
1057
|
-
agentType,
|
|
1058
|
-
command,
|
|
1059
|
-
server,
|
|
1060
|
-
apiKey,
|
|
1061
|
-
vault,
|
|
1062
|
-
vaultMode,
|
|
1063
|
-
description,
|
|
1064
|
-
hooks: defaults.hooks || {},
|
|
1065
|
-
ssh: null,
|
|
1066
|
-
env: {},
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
registryAddAgent(name, entry);
|
|
1070
|
-
console.log(`\n ✓ Added agent "${name}" to registry.`);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// --- Remove command ----------------------------------------------------------
|
|
1074
|
-
|
|
1075
|
-
function runRemove(name) {
|
|
1076
|
-
if (!name) {
|
|
1077
|
-
console.error(" Usage: fathom-mcp remove <name>");
|
|
1078
|
-
process.exit(1);
|
|
1079
|
-
}
|
|
1080
|
-
const removed = registryRemoveAgent(name);
|
|
1081
|
-
if (removed) {
|
|
1082
|
-
console.log(` ✓ Removed agent "${name}" from registry.`);
|
|
1083
|
-
} else {
|
|
1084
|
-
console.error(` Error: No agent "${name}" in registry.`);
|
|
1085
|
-
process.exit(1);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// --- Config command ----------------------------------------------------------
|
|
1090
|
-
|
|
1091
|
-
function runConfigCmd(name) {
|
|
1092
|
-
if (!name) {
|
|
1093
|
-
console.error(" Usage: fathom-mcp config <name>");
|
|
1094
|
-
process.exit(1);
|
|
1095
|
-
}
|
|
1096
|
-
const entry = getAgent(name);
|
|
1097
|
-
if (!entry) {
|
|
1098
|
-
console.error(` Error: No agent "${name}" in registry.`);
|
|
1099
|
-
process.exit(1);
|
|
1100
|
-
}
|
|
1101
|
-
console.log(JSON.stringify({ [name]: entry }, null, 2));
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
938
|
// --- Main --------------------------------------------------------------------
|
|
1105
939
|
|
|
1106
940
|
// Guard: only run CLI when this module is the entry point (not when imported by tests)
|
|
@@ -1122,20 +956,8 @@ if (isMain) {
|
|
|
1122
956
|
asyncHandler(runStatus);
|
|
1123
957
|
} else if (command === "update") {
|
|
1124
958
|
asyncHandler(runUpdate);
|
|
1125
|
-
} else if (command === "start") {
|
|
1126
|
-
runStart(process.argv.slice(3));
|
|
1127
959
|
} else if (command === "list" || command === "ls") {
|
|
1128
|
-
runList
|
|
1129
|
-
} else if (command === "stop") {
|
|
1130
|
-
runStop(process.argv[3]);
|
|
1131
|
-
} else if (command === "restart") {
|
|
1132
|
-
runRestart(process.argv[3]);
|
|
1133
|
-
} else if (command === "add") {
|
|
1134
|
-
asyncHandler(() => runAdd(process.argv.slice(3)));
|
|
1135
|
-
} else if (command === "remove" || command === "rm") {
|
|
1136
|
-
runRemove(process.argv[3]);
|
|
1137
|
-
} else if (command === "config") {
|
|
1138
|
-
runConfigCmd(process.argv[3]);
|
|
960
|
+
asyncHandler(runList);
|
|
1139
961
|
} else if (!command || command === "serve") {
|
|
1140
962
|
import("./index.js");
|
|
1141
963
|
} else {
|
|
@@ -1147,14 +969,7 @@ if (isMain) {
|
|
|
1147
969
|
fathom-mcp init [-y --api-key KEY --vault-mode MODE --agent AGENT] Interactive/non-interactive setup
|
|
1148
970
|
fathom-mcp status Check connection status
|
|
1149
971
|
fathom-mcp update Update hooks + version
|
|
1150
|
-
|
|
1151
|
-
fathom-mcp list List all agents + status
|
|
1152
|
-
fathom-mcp start [name] Start agent (by name or legacy cwd)
|
|
1153
|
-
fathom-mcp stop <name> Stop agent
|
|
1154
|
-
fathom-mcp restart <name> Stop + start agent
|
|
1155
|
-
fathom-mcp add [name] Add agent to registry
|
|
1156
|
-
fathom-mcp remove <name> Remove from registry
|
|
1157
|
-
fathom-mcp config <name> Print agent config JSON`);
|
|
972
|
+
fathom-mcp list List workspaces + status (from server)`);
|
|
1158
973
|
process.exit(1);
|
|
1159
974
|
}
|
|
1160
975
|
}
|
package/src/config.js
CHANGED
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
* Precedence (highest wins):
|
|
5
5
|
* 1. Environment variables (FATHOM_SERVER_URL, FATHOM_API_KEY, FATHOM_WORKSPACE, FATHOM_VAULT_DIR)
|
|
6
6
|
* 2. .fathom.json (walked up from cwd to filesystem root)
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Built-in defaults
|
|
7
|
+
* 3. Built-in defaults
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import fs from "fs";
|
|
12
11
|
import path from "path";
|
|
13
|
-
import { findAgentByDir } from "./agents.js";
|
|
14
12
|
|
|
15
13
|
const CONFIG_FILENAME = ".fathom.json";
|
|
16
14
|
|
|
17
|
-
const VALID_VAULT_MODES = new Set(["
|
|
15
|
+
const VALID_VAULT_MODES = new Set(["synced", "local", "none"]);
|
|
18
16
|
|
|
19
17
|
const DEFAULTS = {
|
|
20
18
|
workspace: "",
|
|
@@ -76,31 +74,13 @@ function applyConfig(result, config) {
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
|
-
* Resolve final config by merging: defaults →
|
|
77
|
+
* Resolve final config by merging: defaults → .fathom.json → env vars.
|
|
80
78
|
*/
|
|
81
79
|
export function resolveConfig(startDir = process.cwd()) {
|
|
82
80
|
const result = { ...DEFAULTS, hooks: { ...DEFAULTS.hooks } };
|
|
83
81
|
let projectDir = startDir;
|
|
84
82
|
|
|
85
|
-
// Layer
|
|
86
|
-
let registryMatch = null;
|
|
87
|
-
try {
|
|
88
|
-
registryMatch = findAgentByDir(startDir);
|
|
89
|
-
} catch {
|
|
90
|
-
// Registry not available — skip
|
|
91
|
-
}
|
|
92
|
-
if (registryMatch) {
|
|
93
|
-
const { name, entry } = registryMatch;
|
|
94
|
-
projectDir = entry.projectDir;
|
|
95
|
-
result.workspace = name;
|
|
96
|
-
applyConfig(result, entry);
|
|
97
|
-
if (entry.agentType) {
|
|
98
|
-
result.agents = [entry.agentType];
|
|
99
|
-
}
|
|
100
|
-
result._registryName = name;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Layer 2: .fathom.json (overrides registry)
|
|
83
|
+
// Layer 2: .fathom.json
|
|
104
84
|
const found = findConfigFile(startDir);
|
|
105
85
|
if (found) {
|
|
106
86
|
projectDir = found.dir;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared frontmatter parsing and validation for vault files.
|
|
3
|
+
*
|
|
4
|
+
* Used by the vault-frontmatter-lint hook script and any other
|
|
5
|
+
* code that needs to validate vault file frontmatter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// --- Constants ---------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const VALID_STATUSES = new Set(["draft", "published", "archived"]);
|
|
11
|
+
|
|
12
|
+
export const VAULT_SCHEMA = {
|
|
13
|
+
title: { required: true, type: "string" },
|
|
14
|
+
date: { required: true, type: "string" },
|
|
15
|
+
tags: { required: false, type: "array" },
|
|
16
|
+
status: { required: false, type: "string" },
|
|
17
|
+
project: { required: false, type: "string" },
|
|
18
|
+
aliases: { required: false, type: "array" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// --- Frontmatter -------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function parseFrontmatter(content) {
|
|
24
|
+
if (!content.startsWith("---")) return { fm: {}, body: content };
|
|
25
|
+
const lines = content.split("\n");
|
|
26
|
+
let endIdx = null;
|
|
27
|
+
for (let i = 1; i < lines.length; i++) {
|
|
28
|
+
if (lines[i].trim() === "---") { endIdx = i; break; }
|
|
29
|
+
}
|
|
30
|
+
if (endIdx === null) return { fm: {}, body: content };
|
|
31
|
+
try {
|
|
32
|
+
const fmLines = lines.slice(1, endIdx);
|
|
33
|
+
const fm = {};
|
|
34
|
+
let currentKey = null;
|
|
35
|
+
for (const line of fmLines) {
|
|
36
|
+
const listMatch = line.match(/^[ ]{2}- (.+)$/);
|
|
37
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
38
|
+
if (listMatch && currentKey) {
|
|
39
|
+
fm[currentKey].push(listMatch[1].trim());
|
|
40
|
+
} else if (kvMatch) {
|
|
41
|
+
currentKey = kvMatch[1];
|
|
42
|
+
const val = kvMatch[2].trim();
|
|
43
|
+
if (val === "") {
|
|
44
|
+
fm[currentKey] = [];
|
|
45
|
+
} else {
|
|
46
|
+
fm[currentKey] = val;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const body = lines.slice(endIdx + 1).join("\n");
|
|
51
|
+
return { fm, body };
|
|
52
|
+
} catch {
|
|
53
|
+
return { fm: {}, body: content };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateFrontmatter(fm) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
for (const [field, spec] of Object.entries(VAULT_SCHEMA)) {
|
|
60
|
+
const val = fm[field];
|
|
61
|
+
if (spec.required && val == null) {
|
|
62
|
+
errors.push(`Missing required field: '${field}'`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (val != null) {
|
|
66
|
+
const actualType = Array.isArray(val) ? "array" : typeof val;
|
|
67
|
+
if (actualType !== spec.type) {
|
|
68
|
+
errors.push(`Field '${field}' must be ${spec.type}, got ${actualType}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const status = fm["status"];
|
|
73
|
+
if (status != null && !VALID_STATUSES.has(status)) {
|
|
74
|
+
errors.push(`Field 'status' must be one of [${[...VALID_STATUSES].join(", ")}], got '${status}'`);
|
|
75
|
+
}
|
|
76
|
+
return errors;
|
|
77
|
+
}
|