fathom-mcp 0.6.3 → 2.0.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/index.js +154 -0
- package/package.json +10 -42
- package/CHANGELOG.md +0 -39
- package/LICENSE +0 -21
- package/README.md +0 -133
- package/fathom-agents.md +0 -45
- package/scripts/fathom-precompact.sh +0 -80
- package/scripts/fathom-recall.sh +0 -194
- package/scripts/fathom-sessionstart.sh +0 -72
- package/scripts/fathom-start.sh +0 -366
- package/scripts/fathom-vsearch-background.sh +0 -46
- package/scripts/hook-toast.sh +0 -139
- package/scripts/kokoro-bridge.py +0 -196
- package/scripts/kokoro-speak.py +0 -107
- package/scripts/setup-kokoro.sh +0 -29
- package/scripts/vault-frontmatter-lint.js +0 -65
- package/src/cli.js +0 -937
- package/src/config.js +0 -137
- package/src/frontmatter.js +0 -77
- package/src/index.js +0 -552
- package/src/server-client.js +0 -303
- package/src/ws-connection.js +0 -156
package/scripts/fathom-recall.sh
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Fathom Vault recall — BM25 keyword + cached semantic search on every user message.
|
|
3
|
-
# JSON output: systemMessage (user sees count) + additionalContext (model sees details).
|
|
4
|
-
#
|
|
5
|
-
# Search strategy:
|
|
6
|
-
# - BM25 keyword search runs synchronously (<2s) for immediate results
|
|
7
|
-
# - Vector semantic search runs asynchronously in the background
|
|
8
|
-
# - Cached vsearch results from the previous query are shown alongside BM25
|
|
9
|
-
# - This gives us the best of both: fast keywords + deep semantics (one message behind)
|
|
10
|
-
|
|
11
|
-
set -o pipefail
|
|
12
|
-
|
|
13
|
-
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
-
TOAST="$HOOK_DIR/hook-toast.sh"
|
|
15
|
-
VSEARCH_CACHE="/tmp/fathom-vsearch-cache.json"
|
|
16
|
-
VSEARCH_LOCK="/tmp/fathom-vsearch.lock"
|
|
17
|
-
STALE_LOCK_SECONDS=180 # 3 minutes — lock older than this is considered stale
|
|
18
|
-
CACHE_TTL_SECONDS=300 # 5 minutes — cached results older than this are ignored
|
|
19
|
-
|
|
20
|
-
# Walk up to find .fathom.json
|
|
21
|
-
find_config() {
|
|
22
|
-
local dir="$PWD"
|
|
23
|
-
while [ "$dir" != "/" ]; do
|
|
24
|
-
if [ -f "$dir/.fathom.json" ]; then
|
|
25
|
-
echo "$dir/.fathom.json"
|
|
26
|
-
return 0
|
|
27
|
-
fi
|
|
28
|
-
dir="$(dirname "$dir")"
|
|
29
|
-
done
|
|
30
|
-
return 1
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
CONFIG_FILE=$(find_config 2>/dev/null) || exit 0
|
|
34
|
-
|
|
35
|
-
WORKSPACE=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('workspace',''))" 2>/dev/null || echo "")
|
|
36
|
-
SERVER=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('server','http://localhost:4243'))" 2>/dev/null || echo "http://localhost:4243")
|
|
37
|
-
API_KEY=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('apiKey',''))" 2>/dev/null || echo "")
|
|
38
|
-
|
|
39
|
-
AUTH_HEADER=""
|
|
40
|
-
if [ -n "$API_KEY" ]; then
|
|
41
|
-
AUTH_HEADER="Authorization: Bearer $API_KEY"
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# Compact parser: converts verbose qmd output to one-line-per-result format
|
|
45
|
-
compact_results() {
|
|
46
|
-
python3 -c "
|
|
47
|
-
import sys, re
|
|
48
|
-
lines = sys.stdin.read().strip().split('\n')
|
|
49
|
-
current = {}
|
|
50
|
-
for line in lines:
|
|
51
|
-
m = re.match(r'qmd://[^/]+/(.+?)(?::\d+)?\s+#\w+', line)
|
|
52
|
-
if m:
|
|
53
|
-
if current and current.get('path'):
|
|
54
|
-
score = current.get('score', '?')
|
|
55
|
-
title = current.get('title', '(untitled)')
|
|
56
|
-
print(f\" {current['path']} ({score}) — {title}\")
|
|
57
|
-
current = {'path': m.group(1)}
|
|
58
|
-
elif line.startswith('Title: '):
|
|
59
|
-
current['title'] = line[7:]
|
|
60
|
-
elif line.startswith('Score:'):
|
|
61
|
-
parts = line.split()
|
|
62
|
-
current['score'] = parts[-1] if parts else '?'
|
|
63
|
-
if current and current.get('path'):
|
|
64
|
-
score = current.get('score', '?')
|
|
65
|
-
title = current.get('title', '(untitled)')
|
|
66
|
-
print(f\" {current['path']} ({score}) — {title}\")
|
|
67
|
-
" 2>/dev/null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
INPUT=$(cat)
|
|
71
|
-
USER_MESSAGE=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
|
|
72
|
-
|
|
73
|
-
if [ -z "$USER_MESSAGE" ] || [ ${#USER_MESSAGE} -lt 10 ]; then
|
|
74
|
-
exit 0
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
|
-
QUERY="${USER_MESSAGE:0:500}"
|
|
78
|
-
|
|
79
|
-
# Toast: start retrieving
|
|
80
|
-
"$TOAST" fathom "⏳ Retrieving docs..." &>/dev/null
|
|
81
|
-
|
|
82
|
-
# --- Phase 1: Read cached vsearch results from previous query ---
|
|
83
|
-
CACHED_VSEARCH=""
|
|
84
|
-
if [ -f "$VSEARCH_CACHE" ]; then
|
|
85
|
-
CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$VSEARCH_CACHE" 2>/dev/null || echo 0) ))
|
|
86
|
-
if [ "$CACHE_AGE" -lt "$CACHE_TTL_SECONDS" ]; then
|
|
87
|
-
RAW_VSEARCH=$(python3 -c "
|
|
88
|
-
import json, sys
|
|
89
|
-
try:
|
|
90
|
-
with open(sys.argv[1]) as f:
|
|
91
|
-
data = json.load(f)
|
|
92
|
-
if data.get('results'):
|
|
93
|
-
print(data['results'])
|
|
94
|
-
except Exception:
|
|
95
|
-
pass
|
|
96
|
-
" "$VSEARCH_CACHE" 2>/dev/null)
|
|
97
|
-
if [ -n "$RAW_VSEARCH" ]; then
|
|
98
|
-
CACHED_VSEARCH=$(echo "$RAW_VSEARCH" | compact_results)
|
|
99
|
-
fi
|
|
100
|
-
fi
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
# --- Phase 2: Run search via API ---
|
|
104
|
-
ENCODED_Q=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
|
|
105
|
-
|
|
106
|
-
CURL_ARGS=(-sf)
|
|
107
|
-
[ -n "$AUTH_HEADER" ] && CURL_ARGS+=(-H "$AUTH_HEADER")
|
|
108
|
-
|
|
109
|
-
API_RESPONSE=$(timeout 5 curl "${CURL_ARGS[@]}" "${SERVER}/api/search?q=${ENCODED_Q}&n=5&mode=bm25&workspace=${WORKSPACE}" 2>/dev/null)
|
|
110
|
-
BM25_RESULTS=""
|
|
111
|
-
|
|
112
|
-
if [ -n "$API_RESPONSE" ]; then
|
|
113
|
-
BM25_RESULTS=$(echo "$API_RESPONSE" | python3 -c "
|
|
114
|
-
import json, sys
|
|
115
|
-
try:
|
|
116
|
-
data = json.load(sys.stdin)
|
|
117
|
-
for r in data.get('results', []):
|
|
118
|
-
score = str(r.get('score', '?')) + '%'
|
|
119
|
-
title = r.get('title', '(untitled)')
|
|
120
|
-
path = r.get('file', '')
|
|
121
|
-
print(f' {path} ({score}) — {title}')
|
|
122
|
-
except Exception:
|
|
123
|
-
pass
|
|
124
|
-
" 2>/dev/null)
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
# Combine vault results
|
|
128
|
-
ALL_RESULTS=""
|
|
129
|
-
[ -n "$BM25_RESULTS" ] && ALL_RESULTS="$BM25_RESULTS"
|
|
130
|
-
if [ -n "$CACHED_VSEARCH" ]; then
|
|
131
|
-
if [ -n "$ALL_RESULTS" ]; then
|
|
132
|
-
ALL_RESULTS="$ALL_RESULTS"$'\n'"$CACHED_VSEARCH"
|
|
133
|
-
else
|
|
134
|
-
ALL_RESULTS="$CACHED_VSEARCH"
|
|
135
|
-
fi
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
# --- Output ---
|
|
139
|
-
if [ -n "$BM25_RESULTS" ] || [ -n "$CACHED_VSEARCH" ]; then
|
|
140
|
-
VAULT_COUNT=0
|
|
141
|
-
[ -n "$BM25_RESULTS" ] && VAULT_COUNT=$(echo "$BM25_RESULTS" | grep -c '^\s' || true)
|
|
142
|
-
[ -n "$CACHED_VSEARCH" ] && VAULT_COUNT=$((VAULT_COUNT + $(echo "$CACHED_VSEARCH" | grep -c '^\s' || true)))
|
|
143
|
-
|
|
144
|
-
DETAIL_TEXT="Fathom Vault: ${VAULT_COUNT} results"
|
|
145
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'
|
|
146
|
-
if [ -n "$BM25_RESULTS" ] && [ -n "$CACHED_VSEARCH" ]; then
|
|
147
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"Vault (keyword):"
|
|
148
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$BM25_RESULTS"
|
|
149
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'$'\n'"Vault (semantic, previous query):"
|
|
150
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$CACHED_VSEARCH"
|
|
151
|
-
elif [ -n "$BM25_RESULTS" ]; then
|
|
152
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$BM25_RESULTS"
|
|
153
|
-
elif [ -n "$CACHED_VSEARCH" ]; then
|
|
154
|
-
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$CACHED_VSEARCH"
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
SUMMARY="Fathom Vault: ${VAULT_COUNT} memories"
|
|
158
|
-
|
|
159
|
-
# Toast: result
|
|
160
|
-
"$TOAST" fathom "✓ ${VAULT_COUNT} docs recalled" &>/dev/null
|
|
161
|
-
|
|
162
|
-
python3 -c "
|
|
163
|
-
import json, sys
|
|
164
|
-
summary = sys.argv[1]
|
|
165
|
-
detail = sys.argv[2]
|
|
166
|
-
print(json.dumps({
|
|
167
|
-
'systemMessage': summary,
|
|
168
|
-
'hookSpecificOutput': {
|
|
169
|
-
'hookEventName': 'UserPromptSubmit',
|
|
170
|
-
'additionalContext': detail
|
|
171
|
-
}
|
|
172
|
-
}))
|
|
173
|
-
" "$SUMMARY" "$DETAIL_TEXT"
|
|
174
|
-
else
|
|
175
|
-
# Toast: no results
|
|
176
|
-
"$TOAST" fathom "✓ No docs matched" &>/dev/null
|
|
177
|
-
fi
|
|
178
|
-
|
|
179
|
-
# --- Phase 3: Launch background vsearch for current query ---
|
|
180
|
-
SHOULD_LAUNCH=true
|
|
181
|
-
|
|
182
|
-
if [ -f "$VSEARCH_LOCK" ]; then
|
|
183
|
-
LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$VSEARCH_LOCK" 2>/dev/null || echo 0) ))
|
|
184
|
-
if [ "$LOCK_AGE" -lt "$STALE_LOCK_SECONDS" ]; then
|
|
185
|
-
SHOULD_LAUNCH=false
|
|
186
|
-
else
|
|
187
|
-
rm -f "$VSEARCH_LOCK"
|
|
188
|
-
fi
|
|
189
|
-
fi
|
|
190
|
-
|
|
191
|
-
if [ "$SHOULD_LAUNCH" = true ]; then
|
|
192
|
-
nohup "$HOOK_DIR/fathom-vsearch-background.sh" "$QUERY" "$WORKSPACE" >/dev/null 2>&1 &
|
|
193
|
-
disown
|
|
194
|
-
fi
|
|
@@ -1,72 +0,0 @@
|
|
|
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/scripts/fathom-start.sh
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# fathom-start.sh — Launch an agent session.
|
|
4
|
-
#
|
|
5
|
-
# Reads .fathom.json for workspace name and agent. For interactive agents
|
|
6
|
-
# (claude-code, codex, gemini, opencode), wraps in a tmux session. For
|
|
7
|
-
# headless agents (claude-sdk), spawns a direct process with PID tracking.
|
|
8
|
-
#
|
|
9
|
-
# Claude agents require an explicit mode flag:
|
|
10
|
-
# fathom-start.sh --claude-code-w-tmux Interactive Claude Code in tmux
|
|
11
|
-
# fathom-start.sh --claude-sdk Headless Claude SDK, no tmux
|
|
12
|
-
#
|
|
13
|
-
# Other usage:
|
|
14
|
-
# fathom-start.sh Start agent (non-claude agents auto-detect)
|
|
15
|
-
# fathom-start.sh --detach Start agent, don't attach to tmux
|
|
16
|
-
# fathom-start.sh --agent X Override agent
|
|
17
|
-
# fathom-start.sh --kill Kill existing session
|
|
18
|
-
# fathom-start.sh --status Show session status
|
|
19
|
-
|
|
20
|
-
set -euo pipefail
|
|
21
|
-
|
|
22
|
-
# ── Defaults ──────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
ATTACH=true
|
|
25
|
-
AGENT_OVERRIDE=""
|
|
26
|
-
ACTION="start"
|
|
27
|
-
MODE_FLAG=""
|
|
28
|
-
USE_TMUX=true
|
|
29
|
-
|
|
30
|
-
# ── Parse flags ───────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
while [[ $# -gt 0 ]]; do
|
|
33
|
-
case "$1" in
|
|
34
|
-
--claude-code-w-tmux) MODE_FLAG="claude-code-w-tmux"; shift ;;
|
|
35
|
-
--claude-sdk) MODE_FLAG="claude-sdk"; shift ;;
|
|
36
|
-
--detach) ATTACH=false; shift ;;
|
|
37
|
-
--attach) ATTACH=true; shift ;;
|
|
38
|
-
--agent) AGENT_OVERRIDE="$2"; shift 2 ;;
|
|
39
|
-
--kill) ACTION="kill"; shift ;;
|
|
40
|
-
--status) ACTION="status"; shift ;;
|
|
41
|
-
-h|--help)
|
|
42
|
-
echo "Usage: fathom-start.sh [FLAGS]"
|
|
43
|
-
echo ""
|
|
44
|
-
echo "Mode flags (required for Claude agents):"
|
|
45
|
-
echo " --claude-code-w-tmux Interactive Claude Code in tmux"
|
|
46
|
-
echo " --claude-sdk Headless Claude SDK, no tmux"
|
|
47
|
-
echo ""
|
|
48
|
-
echo "Other flags:"
|
|
49
|
-
echo " --detach Start but don't attach to tmux"
|
|
50
|
-
echo " --agent X Override agent: claude-code, claude-sdk, codex, gemini, opencode"
|
|
51
|
-
echo " --kill Kill existing session"
|
|
52
|
-
echo " --status Show if session is running"
|
|
53
|
-
exit 0
|
|
54
|
-
;;
|
|
55
|
-
*)
|
|
56
|
-
echo "Unknown flag: $1" >&2
|
|
57
|
-
exit 1
|
|
58
|
-
;;
|
|
59
|
-
esac
|
|
60
|
-
done
|
|
61
|
-
|
|
62
|
-
# ── Find .fathom.json ────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
find_config() {
|
|
65
|
-
local dir="$PWD"
|
|
66
|
-
while [[ "$dir" != "/" ]]; do
|
|
67
|
-
if [[ -f "$dir/.fathom.json" ]]; then
|
|
68
|
-
echo "$dir/.fathom.json"
|
|
69
|
-
return 0
|
|
70
|
-
fi
|
|
71
|
-
dir="$(dirname "$dir")"
|
|
72
|
-
done
|
|
73
|
-
return 1
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
CONFIG_FILE=$(find_config) || {
|
|
77
|
-
echo "Error: No .fathom.json found (searched from $PWD to /)" >&2
|
|
78
|
-
echo "Run 'npx fathom-mcp init' first." >&2
|
|
79
|
-
exit 1
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
PROJECT_DIR="$(dirname "$CONFIG_FILE")"
|
|
83
|
-
|
|
84
|
-
# ── Parse config ──────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
read_json_field() {
|
|
87
|
-
local file="$1" field="$2"
|
|
88
|
-
if command -v jq &>/dev/null; then
|
|
89
|
-
jq -r ".$field // empty" "$file" 2>/dev/null
|
|
90
|
-
else
|
|
91
|
-
# Fallback: simple grep/sed for flat string fields
|
|
92
|
-
sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" | head -1
|
|
93
|
-
fi
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
read_json_array_first() {
|
|
97
|
-
local file="$1" field="$2"
|
|
98
|
-
if command -v jq &>/dev/null; then
|
|
99
|
-
jq -r ".$field[0] // empty" "$file" 2>/dev/null
|
|
100
|
-
else
|
|
101
|
-
# Fallback: grab first quoted string after the array field
|
|
102
|
-
sed -n "/\"$field\"/,/\]/{ s/.*\"\([^\"]*\)\".*/\1/p; }" "$file" | head -1
|
|
103
|
-
fi
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
WORKSPACE=$(read_json_field "$CONFIG_FILE" "workspace")
|
|
107
|
-
if [[ -z "$WORKSPACE" ]]; then
|
|
108
|
-
WORKSPACE="$(basename "$PROJECT_DIR")"
|
|
109
|
-
fi
|
|
110
|
-
|
|
111
|
-
SESSION="${WORKSPACE}_fathom-session"
|
|
112
|
-
PANE_DIR="$HOME/.config/fathom-mcp"
|
|
113
|
-
PANE_FILE="$PANE_DIR/${WORKSPACE}-pane-id"
|
|
114
|
-
PID_FILE="$PROJECT_DIR/.fathom/agent.pid"
|
|
115
|
-
LOG_FILE="$PROJECT_DIR/.fathom/agent.log"
|
|
116
|
-
|
|
117
|
-
# ── Resolve agent type ────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
resolve_agent() {
|
|
120
|
-
local agent="${AGENT_OVERRIDE:-}"
|
|
121
|
-
if [[ -z "$agent" ]]; then
|
|
122
|
-
agent=$(read_json_array_first "$CONFIG_FILE" "agents")
|
|
123
|
-
fi
|
|
124
|
-
if [[ -z "$agent" ]]; then
|
|
125
|
-
agent="claude-code"
|
|
126
|
-
fi
|
|
127
|
-
echo "$agent"
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
AGENT=$(resolve_agent)
|
|
131
|
-
|
|
132
|
-
# ── Determine execution mode (tmux vs headless) ──────────────────────────────
|
|
133
|
-
# Must happen in the main shell, not inside $(...) subshell.
|
|
134
|
-
|
|
135
|
-
case "$AGENT" in
|
|
136
|
-
claude-code)
|
|
137
|
-
if [[ "$MODE_FLAG" == "claude-sdk" ]]; then
|
|
138
|
-
USE_TMUX=false
|
|
139
|
-
elif [[ -z "$MODE_FLAG" ]]; then
|
|
140
|
-
echo "Error: Claude Code requires an explicit mode flag:" >&2
|
|
141
|
-
echo " fathom-start.sh --claude-code-w-tmux Interactive Claude in tmux" >&2
|
|
142
|
-
echo " fathom-start.sh --claude-sdk Headless Claude SDK" >&2
|
|
143
|
-
echo "" >&2
|
|
144
|
-
echo "For local agents managed by fathom-server, use the dashboard restart button instead." >&2
|
|
145
|
-
exit 1
|
|
146
|
-
fi
|
|
147
|
-
;;
|
|
148
|
-
claude-sdk)
|
|
149
|
-
if [[ "$MODE_FLAG" == "claude-code-w-tmux" ]]; then
|
|
150
|
-
USE_TMUX=true
|
|
151
|
-
else
|
|
152
|
-
USE_TMUX=false
|
|
153
|
-
fi
|
|
154
|
-
;;
|
|
155
|
-
esac
|
|
156
|
-
|
|
157
|
-
# ── Resolve agent command ─────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
resolve_agent_cmd() {
|
|
160
|
-
case "$AGENT" in
|
|
161
|
-
claude-code)
|
|
162
|
-
if [[ "$USE_TMUX" == false ]]; then
|
|
163
|
-
echo "claude -p --permission-mode bypassPermissions --output-format stream-json"
|
|
164
|
-
else
|
|
165
|
-
echo "claude --model opus --permission-mode bypassPermissions"
|
|
166
|
-
fi
|
|
167
|
-
;;
|
|
168
|
-
claude-sdk)
|
|
169
|
-
if [[ "$USE_TMUX" == true ]]; then
|
|
170
|
-
echo "claude --model opus --permission-mode bypassPermissions"
|
|
171
|
-
else
|
|
172
|
-
echo "claude -p --permission-mode bypassPermissions --output-format stream-json"
|
|
173
|
-
fi
|
|
174
|
-
;;
|
|
175
|
-
codex)
|
|
176
|
-
echo "codex"
|
|
177
|
-
;;
|
|
178
|
-
gemini)
|
|
179
|
-
echo "gemini"
|
|
180
|
-
;;
|
|
181
|
-
opencode)
|
|
182
|
-
echo "opencode"
|
|
183
|
-
;;
|
|
184
|
-
*)
|
|
185
|
-
echo "Warning: Unknown agent '$AGENT', falling back to claude" >&2
|
|
186
|
-
echo "claude"
|
|
187
|
-
;;
|
|
188
|
-
esac
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
# ── Save pane ID ──────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
save_pane_id() {
|
|
194
|
-
local pane_id
|
|
195
|
-
pane_id=$(tmux list-panes -t "$SESSION" -F '#{pane_id}' 2>/dev/null | head -1)
|
|
196
|
-
if [[ -n "$pane_id" ]]; then
|
|
197
|
-
mkdir -p "$PANE_DIR"
|
|
198
|
-
echo "$pane_id" > "$PANE_FILE"
|
|
199
|
-
echo "Pane ID: $pane_id → $PANE_FILE"
|
|
200
|
-
fi
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# ── Session/process check ────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
session_exists() {
|
|
206
|
-
tmux has-session -t "$SESSION" 2>/dev/null
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
headless_is_running() {
|
|
210
|
-
if [[ -f "$PID_FILE" ]]; then
|
|
211
|
-
local pid
|
|
212
|
-
pid=$(cat "$PID_FILE" 2>/dev/null)
|
|
213
|
-
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
214
|
-
return 0
|
|
215
|
-
fi
|
|
216
|
-
fi
|
|
217
|
-
return 1
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
# ── Actions ───────────────────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
do_status() {
|
|
223
|
-
echo "Workspace: $WORKSPACE"
|
|
224
|
-
echo "Session: $SESSION"
|
|
225
|
-
if session_exists; then
|
|
226
|
-
echo "Status: running (tmux)"
|
|
227
|
-
if [[ -f "$PANE_FILE" ]]; then
|
|
228
|
-
echo "Pane ID: $(cat "$PANE_FILE")"
|
|
229
|
-
fi
|
|
230
|
-
elif headless_is_running; then
|
|
231
|
-
echo "Status: running (headless)"
|
|
232
|
-
echo "PID: $(cat "$PID_FILE")"
|
|
233
|
-
echo "Log: $LOG_FILE"
|
|
234
|
-
if [[ -p "$PROJECT_DIR/.fathom/agent.pipe" ]]; then
|
|
235
|
-
echo "Pipe: $PROJECT_DIR/.fathom/agent.pipe"
|
|
236
|
-
fi
|
|
237
|
-
else
|
|
238
|
-
echo "Status: not running"
|
|
239
|
-
fi
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
do_kill() {
|
|
243
|
-
local killed=false
|
|
244
|
-
if session_exists; then
|
|
245
|
-
tmux kill-session -t "$SESSION"
|
|
246
|
-
rm -f "$PANE_FILE"
|
|
247
|
-
echo "Killed tmux session: $SESSION"
|
|
248
|
-
killed=true
|
|
249
|
-
fi
|
|
250
|
-
if headless_is_running; then
|
|
251
|
-
local pid
|
|
252
|
-
pid=$(cat "$PID_FILE")
|
|
253
|
-
kill "$pid" 2>/dev/null || true
|
|
254
|
-
rm -f "$PID_FILE"
|
|
255
|
-
echo "Killed headless process: PID $pid"
|
|
256
|
-
killed=true
|
|
257
|
-
fi
|
|
258
|
-
# Clean up keeper process and FIFO
|
|
259
|
-
local keeper_pid_file="$PROJECT_DIR/.fathom/agent-keeper.pid"
|
|
260
|
-
if [[ -f "$keeper_pid_file" ]]; then
|
|
261
|
-
local keeper_pid
|
|
262
|
-
keeper_pid=$(cat "$keeper_pid_file")
|
|
263
|
-
kill "$keeper_pid" 2>/dev/null || true
|
|
264
|
-
rm -f "$keeper_pid_file"
|
|
265
|
-
fi
|
|
266
|
-
rm -f "$PROJECT_DIR/.fathom/agent.pipe"
|
|
267
|
-
if [[ "$killed" == false ]]; then
|
|
268
|
-
echo "No running session found for: $WORKSPACE"
|
|
269
|
-
fi
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
do_start() {
|
|
273
|
-
local agent_cmd
|
|
274
|
-
agent_cmd=$(resolve_agent_cmd)
|
|
275
|
-
|
|
276
|
-
if [[ "$USE_TMUX" == false ]]; then
|
|
277
|
-
# ── Headless mode — FIFO-based stdin ──
|
|
278
|
-
if headless_is_running; then
|
|
279
|
-
echo "Headless agent already running (PID $(cat "$PID_FILE"))"
|
|
280
|
-
echo "Log: $LOG_FILE"
|
|
281
|
-
return 0
|
|
282
|
-
fi
|
|
283
|
-
|
|
284
|
-
echo "Starting headless: $agent_cmd"
|
|
285
|
-
echo "Dir: $PROJECT_DIR"
|
|
286
|
-
echo "Logs: $LOG_FILE"
|
|
287
|
-
|
|
288
|
-
cd "$PROJECT_DIR"
|
|
289
|
-
unset CLAUDECODE 2>/dev/null || true
|
|
290
|
-
# Ensure common binary locations are on PATH for backgrounded processes
|
|
291
|
-
export PATH="$HOME/.local/bin:$HOME/.claude/local/bin:$PATH"
|
|
292
|
-
mkdir -p .fathom
|
|
293
|
-
|
|
294
|
-
local pipe_file="$PROJECT_DIR/.fathom/agent.pipe"
|
|
295
|
-
|
|
296
|
-
# Clean up stale pipe
|
|
297
|
-
rm -f "$pipe_file"
|
|
298
|
-
mkfifo "$pipe_file"
|
|
299
|
-
|
|
300
|
-
# Keep a writer FD open so the pipe never sends EOF to the reader.
|
|
301
|
-
# Without this, the agent reads EOF and exits when no writer is connected.
|
|
302
|
-
sleep infinity > "$pipe_file" &
|
|
303
|
-
local keeper_pid=$!
|
|
304
|
-
|
|
305
|
-
# Start agent reading from the FIFO, logging stdout/stderr
|
|
306
|
-
$agent_cmd < "$pipe_file" >> "$LOG_FILE" 2>&1 &
|
|
307
|
-
local pid=$!
|
|
308
|
-
|
|
309
|
-
echo "$pid" > "$PID_FILE"
|
|
310
|
-
echo "$keeper_pid" > "$PROJECT_DIR/.fathom/agent-keeper.pid"
|
|
311
|
-
|
|
312
|
-
echo "PID: $pid"
|
|
313
|
-
echo "Pipe: $pipe_file"
|
|
314
|
-
echo "Started. View logs: tail -f $LOG_FILE"
|
|
315
|
-
echo "Inject: echo 'your message' > $pipe_file"
|
|
316
|
-
return 0
|
|
317
|
-
fi
|
|
318
|
-
|
|
319
|
-
# ── Interactive mode — tmux session ──
|
|
320
|
-
if ! command -v tmux &>/dev/null; then
|
|
321
|
-
echo "Error: tmux is not installed. Install it first:" >&2
|
|
322
|
-
echo " apt install tmux | brew install tmux | dnf install tmux" >&2
|
|
323
|
-
exit 1
|
|
324
|
-
fi
|
|
325
|
-
|
|
326
|
-
if session_exists; then
|
|
327
|
-
echo "Session already running: $SESSION"
|
|
328
|
-
save_pane_id
|
|
329
|
-
if [[ "$ATTACH" == true ]]; then
|
|
330
|
-
exec tmux attach-session -t "$SESSION"
|
|
331
|
-
fi
|
|
332
|
-
return 0
|
|
333
|
-
fi
|
|
334
|
-
|
|
335
|
-
echo "Starting: $SESSION"
|
|
336
|
-
echo "Agent: $agent_cmd"
|
|
337
|
-
echo "Dir: $PROJECT_DIR"
|
|
338
|
-
|
|
339
|
-
# Unset CLAUDECODE to avoid nested session detection
|
|
340
|
-
unset CLAUDECODE 2>/dev/null || true
|
|
341
|
-
|
|
342
|
-
# Create detached tmux session running the agent
|
|
343
|
-
tmux new-session -d -s "$SESSION" -c "$PROJECT_DIR" $agent_cmd
|
|
344
|
-
|
|
345
|
-
# Wait briefly for session to stabilize
|
|
346
|
-
sleep 2
|
|
347
|
-
|
|
348
|
-
if session_exists; then
|
|
349
|
-
save_pane_id
|
|
350
|
-
echo "Session started."
|
|
351
|
-
if [[ "$ATTACH" == true ]]; then
|
|
352
|
-
exec tmux attach-session -t "$SESSION"
|
|
353
|
-
fi
|
|
354
|
-
else
|
|
355
|
-
echo "Error: Session failed to start" >&2
|
|
356
|
-
exit 1
|
|
357
|
-
fi
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
case "$ACTION" in
|
|
363
|
-
status) do_status ;;
|
|
364
|
-
kill) do_kill ;;
|
|
365
|
-
start) do_start ;;
|
|
366
|
-
esac
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Background vector search worker for fathom-recall hook.
|
|
3
|
-
# Launched via nohup/disown — runs asynchronously after the hook returns.
|
|
4
|
-
#
|
|
5
|
-
# Takes a search query as $1 and workspace as $2, runs qmd vsearch,
|
|
6
|
-
# and writes results as JSON to /tmp/fathom-vsearch-cache.json.
|
|
7
|
-
|
|
8
|
-
set -o pipefail
|
|
9
|
-
|
|
10
|
-
LOCK_FILE="/tmp/fathom-vsearch.lock"
|
|
11
|
-
CACHE_FILE="/tmp/fathom-vsearch-cache.json"
|
|
12
|
-
|
|
13
|
-
cleanup() {
|
|
14
|
-
rm -f "$LOCK_FILE"
|
|
15
|
-
}
|
|
16
|
-
trap cleanup EXIT
|
|
17
|
-
|
|
18
|
-
if [ -z "$1" ]; then
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
WORKSPACE="${2:-fathom}"
|
|
23
|
-
|
|
24
|
-
echo $$ > "$LOCK_FILE"
|
|
25
|
-
|
|
26
|
-
VSEARCH_QUERY="${1:0:200}"
|
|
27
|
-
|
|
28
|
-
# Run vsearch with 180-second hard timeout
|
|
29
|
-
RESULTS=$(timeout 180 qmd vsearch "$VSEARCH_QUERY" -n 5 -c "$WORKSPACE" --min-score 0.5 2>/dev/null)
|
|
30
|
-
|
|
31
|
-
TMPFILE=$(mktemp /tmp/fathom-vsearch-cache.XXXXXX)
|
|
32
|
-
|
|
33
|
-
python3 -c "
|
|
34
|
-
import json, sys, time
|
|
35
|
-
query = sys.argv[1][:200]
|
|
36
|
-
results = sys.stdin.read().strip()
|
|
37
|
-
has_results = bool(results) and 'No results found' not in results
|
|
38
|
-
with open(sys.argv[2], 'w') as f:
|
|
39
|
-
json.dump({
|
|
40
|
-
'query': query,
|
|
41
|
-
'timestamp': int(time.time()),
|
|
42
|
-
'results': results if has_results else None
|
|
43
|
-
}, f)
|
|
44
|
-
" "$VSEARCH_QUERY" "$TMPFILE" <<< "$RESULTS"
|
|
45
|
-
|
|
46
|
-
mv "$TMPFILE" "$CACHE_FILE"
|