fathom-mcp 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/package.json +43 -0
- package/scripts/fathom-context.sh +65 -0
- package/scripts/fathom-precompact.sh +68 -0
- package/src/cli.js +347 -0
- package/src/config.js +112 -0
- package/src/index.js +460 -0
- package/src/server-client.js +170 -0
- package/src/vault-ops.js +386 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-02-25)
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- 16 MCP tools: vault read/write/append, image ops, search (BM25/vector/hybrid), rooms, workspaces
|
|
8
|
+
- CLI: `npx fathom-mcp init` setup wizard, `npx fathom-mcp status`
|
|
9
|
+
- Config resolution: `.fathom.json` → env vars → defaults
|
|
10
|
+
- Hook scripts: SessionStart context injection, PreCompact vault snapshot
|
|
11
|
+
- Direct file I/O for vault operations (no server needed for reads/writes)
|
|
12
|
+
- HTTP client for fathom-server API (search, rooms, workspaces)
|
|
13
|
+
- API key auth support (Bearer token)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Myra Krusemark
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# fathom-mcp
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
▐▘ ▗ ▌
|
|
5
|
+
▜▘▀▌▜▘▛▌▛▌▛▛▌▄▖▛▛▌▛▘▛▌
|
|
6
|
+
▐ █▌▐▖▌▌▙▌▌▌▌ ▌▌▌▙▖▙▌
|
|
7
|
+
▌
|
|
8
|
+
|
|
9
|
+
hifathom.com · fathom@myrakrusemark.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
MCP server for [Fathom](https://hifathom.com) — vault operations, search, rooms, and cross-workspace communication.
|
|
13
|
+
|
|
14
|
+
The MCP tools that let Claude Code interact with your vault. Reads/writes happen locally (fast, no network hop). Search, rooms, and workspace management go through your [fathom-server](https://github.com/myrakrusemark/fathom-vault) instance.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx fathom-mcp init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's it. Restart Claude Code and fathom tools will be available.
|
|
23
|
+
|
|
24
|
+
The init wizard creates:
|
|
25
|
+
- `.fathom.json` — workspace config (server URL, API key, vault path)
|
|
26
|
+
- `.mcp.json` — registers `npx fathom-mcp` as an MCP server
|
|
27
|
+
- `.claude/settings.local.json` — hooks for context injection and precompact snapshots
|
|
28
|
+
- `vault/` — creates the directory if it doesn't exist
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
- **Node.js 18+**
|
|
33
|
+
- **[fathom-server](https://github.com/myrakrusemark/fathom-vault)** running (for search, rooms, and workspace features)
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx fathom-mcp # Start MCP server (stdio — used by .mcp.json)
|
|
39
|
+
npx fathom-mcp init # Interactive setup wizard
|
|
40
|
+
npx fathom-mcp status # Check server connection + workspace status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
### Local (direct file I/O)
|
|
46
|
+
| Tool | Description |
|
|
47
|
+
|------|-------------|
|
|
48
|
+
| `fathom_vault_read` | Read a vault file with parsed frontmatter |
|
|
49
|
+
| `fathom_vault_write` | Create or overwrite a vault file (validates frontmatter) |
|
|
50
|
+
| `fathom_vault_append` | Append to a vault file (auto-creates with frontmatter if new) |
|
|
51
|
+
| `fathom_vault_image` | Read a vault image as base64 |
|
|
52
|
+
| `fathom_vault_write_asset` | Save a base64 image to a vault folder's assets/ |
|
|
53
|
+
|
|
54
|
+
### Server (via fathom-server API)
|
|
55
|
+
| Tool | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `fathom_vault_list` | List vault folders with activity signals |
|
|
58
|
+
| `fathom_vault_folder` | List files in a folder with metadata and previews |
|
|
59
|
+
| `fathom_vault_search` | BM25 keyword search |
|
|
60
|
+
| `fathom_vault_vsearch` | Semantic/vector search |
|
|
61
|
+
| `fathom_vault_query` | Hybrid search (BM25 + vectors + reranking) |
|
|
62
|
+
| `fathom_room_post` | Post to a shared room (supports @mentions) |
|
|
63
|
+
| `fathom_room_read` | Read recent room messages |
|
|
64
|
+
| `fathom_room_list` | List all rooms |
|
|
65
|
+
| `fathom_room_describe` | Set a room's description/topic |
|
|
66
|
+
| `fathom_workspaces` | List all configured workspaces |
|
|
67
|
+
| `fathom_send` | Send a message to another workspace's Claude instance |
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
### `.fathom.json`
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"workspace": "my-project",
|
|
76
|
+
"vault": "vault",
|
|
77
|
+
"server": "http://localhost:4243",
|
|
78
|
+
"apiKey": "fv_abc123...",
|
|
79
|
+
"hooks": {
|
|
80
|
+
"context-inject": { "enabled": true },
|
|
81
|
+
"precompact-snapshot": { "enabled": true }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Resolution order (highest priority first)
|
|
87
|
+
|
|
88
|
+
1. Environment variables: `FATHOM_SERVER_URL`, `FATHOM_API_KEY`, `FATHOM_WORKSPACE`, `FATHOM_VAULT_DIR`
|
|
89
|
+
2. `.fathom.json` (walked up from cwd to filesystem root)
|
|
90
|
+
3. Built-in defaults
|
|
91
|
+
|
|
92
|
+
## Hooks
|
|
93
|
+
|
|
94
|
+
**SessionStart / UserPromptSubmit** (`fathom-context.sh`): Injects recent vault activity into Claude's context.
|
|
95
|
+
|
|
96
|
+
**PreCompact** (`fathom-precompact.sh`): Records which vault files were active in the session before context compaction.
|
|
97
|
+
|
|
98
|
+
## Vault Frontmatter Schema
|
|
99
|
+
|
|
100
|
+
Files can optionally include YAML frontmatter:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
---
|
|
104
|
+
title: My Note # required (string)
|
|
105
|
+
date: 2026-02-25 # required (string, YYYY-MM-DD)
|
|
106
|
+
tags: # optional (list)
|
|
107
|
+
- research
|
|
108
|
+
- identity
|
|
109
|
+
status: draft # optional: draft | published | archived
|
|
110
|
+
project: my-project # optional (string)
|
|
111
|
+
aliases: # optional (list)
|
|
112
|
+
- alt-name
|
|
113
|
+
---
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fathom-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fathom-mcp": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"scripts/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node src/index.js",
|
|
19
|
+
"test": "node --test 'test/*.test.js'",
|
|
20
|
+
"lint": "eslint src/ test/",
|
|
21
|
+
"lint:fix": "eslint --fix src/ test/"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
25
|
+
"js-yaml": "^4.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^9.39.3",
|
|
29
|
+
"eslint": "^9.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"vault",
|
|
38
|
+
"fathom",
|
|
39
|
+
"claude",
|
|
40
|
+
"ai-agent",
|
|
41
|
+
"memory"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fathom SessionStart/UserPromptSubmit hook — injects vault context.
|
|
3
|
+
#
|
|
4
|
+
# Reads .fathom.json to find vault path and server URL.
|
|
5
|
+
# On SessionStart: injects recent heartbeat + active vault folders.
|
|
6
|
+
# On UserPromptSubmit: searches vault for relevant context.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# Walk up to find .fathom.json
|
|
11
|
+
find_config() {
|
|
12
|
+
local dir="$PWD"
|
|
13
|
+
while [ "$dir" != "/" ]; do
|
|
14
|
+
if [ -f "$dir/.fathom.json" ]; then
|
|
15
|
+
echo "$dir/.fathom.json"
|
|
16
|
+
return 0
|
|
17
|
+
fi
|
|
18
|
+
dir="$(dirname "$dir")"
|
|
19
|
+
done
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
CONFIG_FILE=$(find_config 2>/dev/null) || exit 0
|
|
24
|
+
|
|
25
|
+
# Extract config values
|
|
26
|
+
WORKSPACE=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('workspace',''))" 2>/dev/null || echo "")
|
|
27
|
+
SERVER=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('server','http://localhost:4243'))" 2>/dev/null || echo "http://localhost:4243")
|
|
28
|
+
API_KEY=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('apiKey',''))" 2>/dev/null || echo "")
|
|
29
|
+
HOOK_ENABLED=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('hooks',{}).get('context-inject',{}).get('enabled','true'))" 2>/dev/null || echo "true")
|
|
30
|
+
|
|
31
|
+
if [ "$HOOK_ENABLED" != "True" ] && [ "$HOOK_ENABLED" != "true" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Read user prompt from stdin (if UserPromptSubmit)
|
|
36
|
+
INPUT=$(cat)
|
|
37
|
+
|
|
38
|
+
# Build auth header
|
|
39
|
+
AUTH_HEADER=""
|
|
40
|
+
if [ -n "$API_KEY" ]; then
|
|
41
|
+
AUTH_HEADER="Authorization: Bearer $API_KEY"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Try to get recent vault activity from server
|
|
45
|
+
CONTEXT=""
|
|
46
|
+
if [ -n "$SERVER" ]; then
|
|
47
|
+
ACTIVITY=$(curl -sf -H "$AUTH_HEADER" \
|
|
48
|
+
"${SERVER}/api/vault/activity?workspace=${WORKSPACE}&limit=5" 2>/dev/null || echo "")
|
|
49
|
+
if [ -n "$ACTIVITY" ]; then
|
|
50
|
+
CONTEXT="Recent vault activity:\n$ACTIVITY"
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [ -n "$CONTEXT" ]; then
|
|
55
|
+
# Output as hook response
|
|
56
|
+
python3 -c "
|
|
57
|
+
import json, sys
|
|
58
|
+
result = {
|
|
59
|
+
'hookSpecificOutput': {
|
|
60
|
+
'additionalContext': '''$CONTEXT'''
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
json.dump(result, sys.stdout)
|
|
64
|
+
"
|
|
65
|
+
fi
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fathom PreCompact hook — snapshots vault state before context compaction.
|
|
3
|
+
#
|
|
4
|
+
# Reads the transcript, extracts any vault file paths mentioned,
|
|
5
|
+
# and records which files were active in this session for continuity.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Walk up to find .fathom.json
|
|
10
|
+
find_config() {
|
|
11
|
+
local dir="$PWD"
|
|
12
|
+
while [ "$dir" != "/" ]; do
|
|
13
|
+
if [ -f "$dir/.fathom.json" ]; then
|
|
14
|
+
echo "$dir/.fathom.json"
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
dir="$(dirname "$dir")"
|
|
18
|
+
done
|
|
19
|
+
return 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CONFIG_FILE=$(find_config 2>/dev/null) || exit 0
|
|
23
|
+
|
|
24
|
+
WORKSPACE=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('workspace',''))" 2>/dev/null || echo "")
|
|
25
|
+
SERVER=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('server','http://localhost:4243'))" 2>/dev/null || echo "http://localhost:4243")
|
|
26
|
+
API_KEY=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('apiKey',''))" 2>/dev/null || echo "")
|
|
27
|
+
HOOK_ENABLED=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('hooks',{}).get('precompact-snapshot',{}).get('enabled','true'))" 2>/dev/null || echo "true")
|
|
28
|
+
|
|
29
|
+
if [ "$HOOK_ENABLED" != "True" ] && [ "$HOOK_ENABLED" != "true" ]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Read PreCompact input (contains transcript_path)
|
|
34
|
+
INPUT=$(cat)
|
|
35
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || echo "")
|
|
36
|
+
|
|
37
|
+
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Extract vault file paths from transcript
|
|
42
|
+
VAULT_FILES=$(grep -oP 'vault/[a-zA-Z0-9_/.-]+\.md' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u || echo "")
|
|
43
|
+
|
|
44
|
+
if [ -z "$VAULT_FILES" ]; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Notify server about accessed files (best-effort)
|
|
49
|
+
AUTH_HEADER=""
|
|
50
|
+
if [ -n "$API_KEY" ]; then
|
|
51
|
+
AUTH_HEADER="Authorization: Bearer $API_KEY"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
for FILE in $VAULT_FILES; do
|
|
55
|
+
curl -sf -X POST -H "Content-Type: application/json" -H "$AUTH_HEADER" \
|
|
56
|
+
-d "{\"path\": \"$FILE\"}" \
|
|
57
|
+
"${SERVER}/api/vault/access?workspace=${WORKSPACE}" >/dev/null 2>&1 || true
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
# Output summary
|
|
61
|
+
FILE_COUNT=$(echo "$VAULT_FILES" | wc -l)
|
|
62
|
+
python3 -c "
|
|
63
|
+
import json, sys
|
|
64
|
+
result = {
|
|
65
|
+
'systemMessage': 'Fathom: Recorded ${FILE_COUNT} vault file(s) from this session.'
|
|
66
|
+
}
|
|
67
|
+
json.dump(result, sys.stdout)
|
|
68
|
+
"
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fathom-mcp CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx fathom-mcp — Start MCP server (stdio, for .mcp.json)
|
|
8
|
+
* npx fathom-mcp init — Interactive setup wizard
|
|
9
|
+
* npx fathom-mcp status — Check server connection + workspace status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import readline from "readline";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
|
|
17
|
+
import { resolveConfig, writeConfig, findConfigFile } from "./config.js";
|
|
18
|
+
import { createClient } from "./server-client.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const SCRIPTS_DIR = path.join(__dirname, "..", "scripts");
|
|
22
|
+
|
|
23
|
+
// --- Helpers -----------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function ask(rl, question, defaultVal = "") {
|
|
26
|
+
const suffix = defaultVal ? ` (${defaultVal})` : "";
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
29
|
+
resolve(answer.trim() || defaultVal);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function askYesNo(rl, question, defaultYes = true) {
|
|
35
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
rl.question(`${question} [${hint}]: `, (answer) => {
|
|
38
|
+
const a = answer.trim().toLowerCase();
|
|
39
|
+
if (!a) return resolve(defaultYes);
|
|
40
|
+
resolve(a === "y" || a === "yes");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Deep merge obj2 into obj1 (mutates obj1). Arrays are replaced, not merged.
|
|
47
|
+
*/
|
|
48
|
+
function deepMerge(obj1, obj2) {
|
|
49
|
+
for (const key of Object.keys(obj2)) {
|
|
50
|
+
if (
|
|
51
|
+
obj1[key] &&
|
|
52
|
+
typeof obj1[key] === "object" &&
|
|
53
|
+
!Array.isArray(obj1[key]) &&
|
|
54
|
+
typeof obj2[key] === "object" &&
|
|
55
|
+
!Array.isArray(obj2[key])
|
|
56
|
+
) {
|
|
57
|
+
deepMerge(obj1[key], obj2[key]);
|
|
58
|
+
} else {
|
|
59
|
+
obj1[key] = obj2[key];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return obj1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonFile(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeJsonFile(filePath, data) {
|
|
74
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function appendToGitignore(dir, patterns) {
|
|
79
|
+
const gitignorePath = path.join(dir, ".gitignore");
|
|
80
|
+
let existing = "";
|
|
81
|
+
try {
|
|
82
|
+
existing = fs.readFileSync(gitignorePath, "utf-8");
|
|
83
|
+
} catch { /* file doesn't exist */ }
|
|
84
|
+
|
|
85
|
+
const missing = patterns.filter((p) => !existing.includes(p));
|
|
86
|
+
if (missing.length > 0) {
|
|
87
|
+
const suffix = existing.endsWith("\n") || !existing ? "" : "\n";
|
|
88
|
+
fs.appendFileSync(gitignorePath, suffix + missing.join("\n") + "\n");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function copyScripts(targetDir) {
|
|
93
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
94
|
+
try {
|
|
95
|
+
const files = fs.readdirSync(SCRIPTS_DIR);
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const src = path.join(SCRIPTS_DIR, file);
|
|
98
|
+
const dest = path.join(targetDir, file);
|
|
99
|
+
fs.copyFileSync(src, dest);
|
|
100
|
+
fs.chmodSync(dest, 0o755);
|
|
101
|
+
}
|
|
102
|
+
return files;
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Init wizard -------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
async function runInit() {
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
+
|
|
114
|
+
console.log(`
|
|
115
|
+
▐▘ ▗ ▌
|
|
116
|
+
▜▘▀▌▜▘▛▌▛▌▛▛▌▄▖▛▛▌▛▘▛▌
|
|
117
|
+
▐ █▌▐▖▌▌▙▌▌▌▌ ▌▌▌▙▖▙▌
|
|
118
|
+
▌
|
|
119
|
+
|
|
120
|
+
hifathom.com · fathom@myrakrusemark.com
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
// Check for existing config
|
|
124
|
+
const existing = findConfigFile(cwd);
|
|
125
|
+
if (existing) {
|
|
126
|
+
console.log(` Found existing config at: ${existing.path}`);
|
|
127
|
+
const proceed = await askYesNo(rl, " Overwrite?", false);
|
|
128
|
+
if (!proceed) {
|
|
129
|
+
console.log(" Aborted.");
|
|
130
|
+
rl.close();
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 1. Workspace name
|
|
136
|
+
const defaultName = path.basename(cwd);
|
|
137
|
+
const workspace = await ask(rl, " Workspace name", defaultName);
|
|
138
|
+
|
|
139
|
+
// 2. Vault subdirectory
|
|
140
|
+
const vault = await ask(rl, " Vault subdirectory", "vault");
|
|
141
|
+
|
|
142
|
+
// 3. Server URL
|
|
143
|
+
const serverUrl = await ask(rl, " Fathom server URL", "http://localhost:4243");
|
|
144
|
+
|
|
145
|
+
// 4. API key
|
|
146
|
+
let apiKey = "";
|
|
147
|
+
const tryFetch = await askYesNo(rl, " Fetch API key from server?", true);
|
|
148
|
+
if (tryFetch) {
|
|
149
|
+
console.log(" Connecting to server...");
|
|
150
|
+
const tmpClient = createClient({ server: serverUrl, apiKey: "", workspace });
|
|
151
|
+
const isUp = await tmpClient.healthCheck();
|
|
152
|
+
if (isUp) {
|
|
153
|
+
const keyResp = await tmpClient.getApiKey();
|
|
154
|
+
if (keyResp.api_key) {
|
|
155
|
+
apiKey = keyResp.api_key;
|
|
156
|
+
console.log(` Got API key: ${apiKey.slice(0, 7)}...${apiKey.slice(-4)}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(" Could not fetch key (auth may not be configured yet).");
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log(" Server not reachable. You can add the API key to .fathom.json later.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
apiKey = await ask(rl, " API key (or leave blank)", "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 5. Hooks
|
|
169
|
+
const enableContextHook = await askYesNo(rl, " Enable SessionStart context injection hook?", true);
|
|
170
|
+
const enablePrecompactHook = await askYesNo(rl, " Enable PreCompact vault snapshot hook?", true);
|
|
171
|
+
|
|
172
|
+
rl.close();
|
|
173
|
+
|
|
174
|
+
// --- Write files ---
|
|
175
|
+
|
|
176
|
+
console.log("\n Creating files...\n");
|
|
177
|
+
|
|
178
|
+
// .fathom.json
|
|
179
|
+
const configData = {
|
|
180
|
+
workspace,
|
|
181
|
+
vault,
|
|
182
|
+
server: serverUrl,
|
|
183
|
+
apiKey,
|
|
184
|
+
hooks: {
|
|
185
|
+
"context-inject": { enabled: enableContextHook },
|
|
186
|
+
"precompact-snapshot": { enabled: enablePrecompactHook },
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const configPath = writeConfig(cwd, configData);
|
|
190
|
+
console.log(` ✓ ${path.relative(cwd, configPath)}`);
|
|
191
|
+
|
|
192
|
+
// .fathom/scripts/
|
|
193
|
+
const scriptsDir = path.join(cwd, ".fathom", "scripts");
|
|
194
|
+
const copiedScripts = copyScripts(scriptsDir);
|
|
195
|
+
if (copiedScripts.length > 0) {
|
|
196
|
+
console.log(` ✓ .fathom/scripts/ (${copiedScripts.length} scripts)`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// vault/ directory
|
|
200
|
+
const vaultDir = path.join(cwd, vault);
|
|
201
|
+
if (!fs.existsSync(vaultDir)) {
|
|
202
|
+
fs.mkdirSync(vaultDir, { recursive: true });
|
|
203
|
+
console.log(` ✓ ${vault}/ (created)`);
|
|
204
|
+
} else {
|
|
205
|
+
console.log(` · ${vault}/ (already exists)`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// .mcp.json
|
|
209
|
+
const mcpJsonPath = path.join(cwd, ".mcp.json");
|
|
210
|
+
const mcpJson = readJsonFile(mcpJsonPath) || {};
|
|
211
|
+
deepMerge(mcpJson, {
|
|
212
|
+
mcpServers: {
|
|
213
|
+
"fathom-vault": {
|
|
214
|
+
command: "npx",
|
|
215
|
+
args: ["-y", "fathom-mcp"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
writeJsonFile(mcpJsonPath, mcpJson);
|
|
220
|
+
console.log(" ✓ .mcp.json");
|
|
221
|
+
|
|
222
|
+
// .claude/settings.local.json — hook registrations
|
|
223
|
+
const claudeSettingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
224
|
+
const claudeSettings = readJsonFile(claudeSettingsPath) || {};
|
|
225
|
+
|
|
226
|
+
const hooks = {};
|
|
227
|
+
if (enableContextHook) {
|
|
228
|
+
hooks["UserPromptSubmit"] = [
|
|
229
|
+
...(claudeSettings.hooks?.["UserPromptSubmit"] || []),
|
|
230
|
+
];
|
|
231
|
+
// Avoid duplicate
|
|
232
|
+
const contextCmd = "bash .fathom/scripts/fathom-context.sh";
|
|
233
|
+
if (!hooks["UserPromptSubmit"].some((h) => h.command === contextCmd)) {
|
|
234
|
+
hooks["UserPromptSubmit"].push({
|
|
235
|
+
type: "command",
|
|
236
|
+
command: contextCmd,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (enablePrecompactHook) {
|
|
241
|
+
hooks["PreCompact"] = [
|
|
242
|
+
...(claudeSettings.hooks?.["PreCompact"] || []),
|
|
243
|
+
];
|
|
244
|
+
const precompactCmd = "bash .fathom/scripts/fathom-precompact.sh";
|
|
245
|
+
if (!hooks["PreCompact"].some((h) => h.command === precompactCmd)) {
|
|
246
|
+
hooks["PreCompact"].push({
|
|
247
|
+
type: "command",
|
|
248
|
+
command: precompactCmd,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (Object.keys(hooks).length > 0) {
|
|
254
|
+
claudeSettings.hooks = { ...(claudeSettings.hooks || {}), ...hooks };
|
|
255
|
+
writeJsonFile(claudeSettingsPath, claudeSettings);
|
|
256
|
+
console.log(" ✓ .claude/settings.local.json (hooks)");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// .gitignore
|
|
260
|
+
appendToGitignore(cwd, [".fathom.json", ".fathom/scripts/"]);
|
|
261
|
+
console.log(" ✓ .gitignore");
|
|
262
|
+
|
|
263
|
+
// Register with server (best-effort)
|
|
264
|
+
if (serverUrl) {
|
|
265
|
+
const regClient = createClient({ server: serverUrl, apiKey, workspace });
|
|
266
|
+
const isUp = await regClient.healthCheck();
|
|
267
|
+
if (isUp) {
|
|
268
|
+
const regResult = await regClient.registerWorkspace(workspace, cwd);
|
|
269
|
+
if (regResult.ok) {
|
|
270
|
+
console.log(` ✓ Registered workspace "${workspace}" with server`);
|
|
271
|
+
} else if (regResult.error) {
|
|
272
|
+
console.log(` · Server: ${regResult.error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(`
|
|
278
|
+
Done! Fathom MCP is configured for workspace "${workspace}".
|
|
279
|
+
|
|
280
|
+
Next steps:
|
|
281
|
+
1. Start the server: cd fathom-server && python app.py
|
|
282
|
+
2. Open Claude Code in this directory — fathom tools will be available.
|
|
283
|
+
3. Add the following to your CLAUDE.md:
|
|
284
|
+
|
|
285
|
+
## Fathom Vault
|
|
286
|
+
Load fathom tools on startup: \`ToolSearch query="+fathom" max_results=20\`
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Status command ----------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
async function runStatus() {
|
|
293
|
+
const config = resolveConfig();
|
|
294
|
+
const client = createClient(config);
|
|
295
|
+
|
|
296
|
+
console.log("\n Fathom MCP Status\n");
|
|
297
|
+
console.log(` Config: ${config._configPath || "(not found — using defaults)"}`);
|
|
298
|
+
console.log(` Workspace: ${config.workspace}`);
|
|
299
|
+
console.log(` Vault: ${config.vault}`);
|
|
300
|
+
console.log(` Server: ${config.server}`);
|
|
301
|
+
console.log(` API Key: ${config.apiKey ? config.apiKey.slice(0, 7) + "..." + config.apiKey.slice(-4) : "(not set)"}`);
|
|
302
|
+
|
|
303
|
+
// Check vault directory
|
|
304
|
+
const vaultExists = fs.existsSync(config.vault);
|
|
305
|
+
console.log(`\n Vault dir: ${vaultExists ? "✓ exists" : "✗ not found"}`);
|
|
306
|
+
|
|
307
|
+
// Check server
|
|
308
|
+
const isUp = await client.healthCheck();
|
|
309
|
+
console.log(` Server: ${isUp ? "✓ reachable" : "✗ not reachable"}`);
|
|
310
|
+
|
|
311
|
+
if (isUp) {
|
|
312
|
+
const wsResult = await client.listWorkspaces();
|
|
313
|
+
if (wsResult.profiles) {
|
|
314
|
+
const names = Object.keys(wsResult.profiles);
|
|
315
|
+
console.log(` Workspaces: ${names.join(", ") || "(none)"}`);
|
|
316
|
+
for (const [name, profile] of Object.entries(wsResult.profiles)) {
|
|
317
|
+
const status = profile.running ? "running" : "stopped";
|
|
318
|
+
console.log(` ${name}: ${status}${profile.model ? ` (${profile.model})` : ""}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- Main --------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
const command = process.argv[2];
|
|
329
|
+
|
|
330
|
+
if (command === "init") {
|
|
331
|
+
runInit().catch((e) => {
|
|
332
|
+
console.error(`Error: ${e.message}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
});
|
|
335
|
+
} else if (command === "status") {
|
|
336
|
+
runStatus().catch((e) => {
|
|
337
|
+
console.error(`Error: ${e.message}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
|
340
|
+
} else if (!command || command === "serve") {
|
|
341
|
+
// Default: start MCP server
|
|
342
|
+
import("./index.js");
|
|
343
|
+
} else {
|
|
344
|
+
console.error(`Unknown command: ${command}`);
|
|
345
|
+
console.error("Usage: fathom-mcp [init|status|serve]");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|