fathom-mcp 0.6.2 → 0.6.4
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-instructions.sh +84 -0
- package/src/cli.js +11 -114
- package/src/index.js +1 -176
- package/src/server-client.js +0 -47
- package/src/ws-connection.js +2 -68
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SessionStart hook (Fathom) — inject MCP tool reference + notification instructions.
|
|
3
|
+
# Independent of version check — always runs.
|
|
4
|
+
# Output: JSON with hookSpecificOutput.additionalContext.
|
|
5
|
+
|
|
6
|
+
# Consume stdin (SessionStart sends JSON we don't need)
|
|
7
|
+
cat > /dev/null
|
|
8
|
+
|
|
9
|
+
read -r -d '' INSTRUCTIONS << 'FATHOM_EOF'
|
|
10
|
+
# Fathom MCP (`mcp__fathom__*` + `mcp__fathom-vault__*`)
|
|
11
|
+
|
|
12
|
+
**Load tools:** `ToolSearch query="+fathom" max_results=20`
|
|
13
|
+
|
|
14
|
+
One MCP server: `fathom-vault`. Handles search, rooms, workspaces, TTS, and routines. Vault file I/O uses Claude Code's native tools (`Read`, `Write`, `Edit`, `Glob`) — a PostToolUse hook validates frontmatter automatically.
|
|
15
|
+
|
|
16
|
+
**`mcp__fathom-vault__*` tools:**
|
|
17
|
+
|
|
18
|
+
| Tool | What it does |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| **Search** | |
|
|
21
|
+
| `fathom_vault_search` | Keyword search (BM25) across vault files — fast, start here |
|
|
22
|
+
| `fathom_vault_vsearch` | Semantic/vector search — conceptual similarity, slower |
|
|
23
|
+
| `fathom_vault_query` | Hybrid search (BM25 + vector + reranking) — most thorough |
|
|
24
|
+
| **Rooms** | |
|
|
25
|
+
| `fathom_room_post` | Post to a shared room (created on first post). Multilateral — all participants see it. Supports `@workspace` mentions and `@all`. |
|
|
26
|
+
| `fathom_room_read` | Read recent room messages (default 60min window anchored to latest). Supports `minutes`/`start` for pagination. |
|
|
27
|
+
| `fathom_room_list` | List all rooms with message count, last activity, unread counts. |
|
|
28
|
+
| `fathom_room_describe` | Set or clear a room's description/topic. |
|
|
29
|
+
| **Workspaces** | |
|
|
30
|
+
| `fathom_workspaces` | List all workspaces with running status, model, and role. |
|
|
31
|
+
| `fathom_send` | DM another workspace — stored in a shared `dm:a+b` room visible to both participants. |
|
|
32
|
+
| **Voice & TTS** | |
|
|
33
|
+
| `fathom_speak` | Generate speech (Kokoro TTS — am_echo 70% + bf_alice 30%). Returns WAV path. Optional `play=true`. |
|
|
34
|
+
| `fathom_send_voice` | Generate speech and send as a voice bubble to Myra via the app. |
|
|
35
|
+
| **Routines** | |
|
|
36
|
+
| `fathom_routine_list` | List routines with merged runtime state (next_ping_at, last_fire_at). Works cross-workspace. |
|
|
37
|
+
| `fathom_routine_fire` | Fire a routine immediately, bypassing conditions. Non-blocking. Works cross-workspace. |
|
|
38
|
+
| **Admin** | |
|
|
39
|
+
| `fathom_key_rotate` | Rotate this agent's API key. Revokes current key, issues new one, reconnects. |
|
|
40
|
+
| `policy_evaluate` | Evaluate a permission request for a tool call (called automatically by --permission-prompt-tool). |
|
|
41
|
+
|
|
42
|
+
**Image Generation (fal.ai):** Use `mcp__fal-ai__*` tools. Default model: `fal-ai/fast-sdxl/image-to-image` for img2img, `fal-ai/nano-banana-pro` for text-to-image. **Always use nano-banana-pro unless another model is specifically requested.**
|
|
43
|
+
|
|
44
|
+
**Browser:** Use `mcp__chrome-devtools__*` tools. The MCP manages Chrome automatically — just call tools directly. **Always close your pages when done** (`close_page`) so other agents can use Chrome. Only one agent can drive Chrome at a time.
|
|
45
|
+
|
|
46
|
+
**Vault structure:** See `vault/CLAUDE.md` for full folder map and what goes where.
|
|
47
|
+
|
|
48
|
+
### App Notifications (#notification room)
|
|
49
|
+
|
|
50
|
+
After posting to #general, also post a structured JSON message to `#notification` for the dashboard feed:
|
|
51
|
+
`fathom_room_post room="notification" message='{"type":"post","title":"...","body":"..."}'`
|
|
52
|
+
Types: `"post"` (findings/content), `"status"` (operational), `"content"` (linked content with `content_id`). Plain text fallback works too.
|
|
53
|
+
|
|
54
|
+
**The body field supports full HTML with inline CSS.** This is a rich rendering surface — use it. Capabilities:
|
|
55
|
+
- **Layout:** `display:flex`, `gap`, `padding`, `margin`, tables — all work. Flexbox is preferred.
|
|
56
|
+
- **Styling:** `border-radius`, `background`, `color`, `font-size`, `font-weight`, `object-fit` — all inline CSS works.
|
|
57
|
+
- **Images:** `<img src="...">` renders inline. Wrap in `<a href="..." target="_blank">` for clickable links to source. Use real CDN image URLs (grab via Chrome `evaluate_script` from product pages, paper figures, screenshots, etc.).
|
|
58
|
+
- **Links:** `<a href="..." target="_blank">` for clickable links. Link images, titles, and add explicit text links — multiple touchpoints per item.
|
|
59
|
+
- **Badges/chips:** Use inline `<span>` with background color, padding, border-radius for sale tags, status pills, warning chips.
|
|
60
|
+
- **Markdown** also works in the body for simpler notifications.
|
|
61
|
+
|
|
62
|
+
**Design your notifications to be visually rich and scannable.** Myra reads these on a dashboard — think product cards, not log entries. Each workspace should develop templates appropriate to its content (e.g., wardrobe uses product cards with images, NS uses equation summaries with paper links).
|
|
63
|
+
|
|
64
|
+
**Attachments:** To attach vault files (images, docs, audio) to a notification, add an `attachments` array. Each entry has `path` (relative to your vault) and optional `label`. The dashboard resolves types from extensions and renders images inline, audio as players, and other files as clickable chips.
|
|
65
|
+
```json
|
|
66
|
+
{"type":"content","content_id":"unique-id","title":"Title","body":"<div style=\"font-family:system-ui\">HTML here</div>","attachments":[
|
|
67
|
+
{"path":"photos/olive-top.jpg","label":"Olive button-up"},
|
|
68
|
+
{"path":"research/note-96.md","label":"Note 96: Seregin Analysis"}
|
|
69
|
+
]}
|
|
70
|
+
```
|
|
71
|
+
Supported: images (.png/.jpg/.gif/.webp), audio (.mp3/.wav/.webm), video (.mp4), documents (.pdf/.md/.txt/.csv/.json/.svg). Old `images` format still works (backward compat).
|
|
72
|
+
FATHOM_EOF
|
|
73
|
+
|
|
74
|
+
python3 -c "
|
|
75
|
+
import json, sys
|
|
76
|
+
print(json.dumps({
|
|
77
|
+
'hookSpecificOutput': {
|
|
78
|
+
'hookEventName': 'SessionStart',
|
|
79
|
+
'additionalContext': sys.argv[1]
|
|
80
|
+
}
|
|
81
|
+
}))
|
|
82
|
+
" "$INSTRUCTIONS"
|
|
83
|
+
|
|
84
|
+
exit 0
|
package/src/cli.js
CHANGED
|
@@ -154,21 +154,6 @@ function copyScripts(targetDir) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function buildIntegrationPrompt(blob) {
|
|
158
|
-
return [
|
|
159
|
-
"The following instructions were generated by fathom-mcp init for this project.",
|
|
160
|
-
"Add them to the file where you store persistent behavioral instructions",
|
|
161
|
-
"(e.g. CLAUDE.md for Claude Code). If the file exists, read it first and",
|
|
162
|
-
"integrate the new section without removing existing content. If a section",
|
|
163
|
-
"with the same heading already exists, replace it. If no instructions file",
|
|
164
|
-
"exists yet, create one.",
|
|
165
|
-
"",
|
|
166
|
-
"--- INSTRUCTIONS ---",
|
|
167
|
-
blob,
|
|
168
|
-
"--- END ---",
|
|
169
|
-
].join("\n");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
157
|
/**
|
|
173
158
|
* Check if fathom-server is available on PATH.
|
|
174
159
|
* Returns "installed" or "not-found".
|
|
@@ -182,29 +167,6 @@ function detectFathomServer() {
|
|
|
182
167
|
}
|
|
183
168
|
}
|
|
184
169
|
|
|
185
|
-
// Agent commands for non-interactive prompt integration (init --non-interactive)
|
|
186
|
-
const AGENT_PROMPT_CMDS = {
|
|
187
|
-
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
188
|
-
"gemini": (prompt) => ["gemini", prompt],
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
function runAgentPrompt(agentKey, prompt) {
|
|
192
|
-
const cmdBuilder = AGENT_PROMPT_CMDS[agentKey];
|
|
193
|
-
if (!cmdBuilder) return null;
|
|
194
|
-
const [cmd, ...args] = cmdBuilder(prompt);
|
|
195
|
-
try {
|
|
196
|
-
const result = execFileSync(cmd, args, {
|
|
197
|
-
cwd: process.cwd(),
|
|
198
|
-
encoding: "utf8",
|
|
199
|
-
stdio: ["pipe", "pipe", "inherit"],
|
|
200
|
-
timeout: 60000,
|
|
201
|
-
});
|
|
202
|
-
return result;
|
|
203
|
-
} catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
170
|
// --- CLI flag parsing --------------------------------------------------------
|
|
209
171
|
|
|
210
172
|
function parseFlags(argv) {
|
|
@@ -572,6 +534,7 @@ async function runInit(flags = {}) {
|
|
|
572
534
|
}
|
|
573
535
|
|
|
574
536
|
// Hook scripts (central location, shared across agents)
|
|
537
|
+
const instructionsCmd = "bash ~/.config/fathom-mcp/scripts/fathom-instructions.sh";
|
|
575
538
|
const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
|
|
576
539
|
const recallCmd = "bash ~/.config/fathom-mcp/scripts/fathom-recall.sh";
|
|
577
540
|
const precompactCmd = "bash ~/.config/fathom-mcp/scripts/fathom-precompact.sh";
|
|
@@ -582,6 +545,8 @@ async function runInit(flags = {}) {
|
|
|
582
545
|
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
583
546
|
const settings = readJsonFile(settingsPath) || {};
|
|
584
547
|
let changed = ensurePermissions(settings);
|
|
548
|
+
// Instructions hook always registered (injects tool reference tables)
|
|
549
|
+
changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
|
|
585
550
|
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
586
551
|
if (enableRecallHook) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 10000) || changed;
|
|
587
552
|
if (enablePrecompactHook) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
|
|
@@ -596,7 +561,8 @@ async function runInit(flags = {}) {
|
|
|
596
561
|
if (hasGemini) {
|
|
597
562
|
const settingsPath = path.join(cwd, ".gemini", "settings.json");
|
|
598
563
|
const settings = readJsonFile(settingsPath) || {};
|
|
599
|
-
let changed = ensureHook(settings, "SessionStart",
|
|
564
|
+
let changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000);
|
|
565
|
+
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
600
566
|
if (enableRecallHook) changed = ensureHook(settings, "BeforeAgent", recallCmd, 10000) || changed;
|
|
601
567
|
if (enablePrecompactHook) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
|
|
602
568
|
if (changed) {
|
|
@@ -683,79 +649,6 @@ async function runInit(flags = {}) {
|
|
|
683
649
|
console.log(`\n Non-interactive equivalent:\n ${parts.join(" ")}\n`);
|
|
684
650
|
}
|
|
685
651
|
|
|
686
|
-
// Auto-integrate agent instructions
|
|
687
|
-
const agentMdPath = agentMdDest;
|
|
688
|
-
let instructionsBlob = "";
|
|
689
|
-
try {
|
|
690
|
-
instructionsBlob = fs.readFileSync(agentMdPath, "utf-8");
|
|
691
|
-
} catch { /* file wasn't created — use empty */ }
|
|
692
|
-
|
|
693
|
-
const primaryAgent = selectedAgents[0];
|
|
694
|
-
|
|
695
|
-
if (instructionsBlob) {
|
|
696
|
-
const prompt = buildIntegrationPrompt(instructionsBlob);
|
|
697
|
-
const cmdBuilder = AGENT_PROMPT_CMDS[primaryAgent];
|
|
698
|
-
|
|
699
|
-
if (nonInteractive) {
|
|
700
|
-
if (cmdBuilder) {
|
|
701
|
-
console.log(`\n Integrating instructions via ${AGENTS[primaryAgent].name}...`);
|
|
702
|
-
const result = runAgentPrompt(primaryAgent, prompt);
|
|
703
|
-
if (result !== null) {
|
|
704
|
-
console.log(result);
|
|
705
|
-
} else {
|
|
706
|
-
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
707
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
708
|
-
}
|
|
709
|
-
} else {
|
|
710
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
711
|
-
}
|
|
712
|
-
} else {
|
|
713
|
-
if (cmdBuilder) {
|
|
714
|
-
const [cmd, ...sampleArgs] = cmdBuilder("<prompt>");
|
|
715
|
-
const flagArgs = sampleArgs.slice(0, -1).join(" ");
|
|
716
|
-
const displayCmd = `${cmd} ${flagArgs} <prompt>`;
|
|
717
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
718
|
-
console.log("\n" + "─".repeat(60));
|
|
719
|
-
const integrate = await askYesNo(
|
|
720
|
-
rl2,
|
|
721
|
-
`\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n ⚠ This uses --dangerously-skip-permissions so the agent can\n write to CLAUDE.md without prompting. If you prefer, decline\n and we'll print the instructions for you to add manually.\n\n Proceed?`,
|
|
722
|
-
true,
|
|
723
|
-
);
|
|
724
|
-
rl2.close();
|
|
725
|
-
|
|
726
|
-
if (integrate) {
|
|
727
|
-
console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
|
|
728
|
-
const result = runAgentPrompt(primaryAgent, prompt);
|
|
729
|
-
if (result !== null) {
|
|
730
|
-
console.log(result);
|
|
731
|
-
} else {
|
|
732
|
-
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
733
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
734
|
-
}
|
|
735
|
-
} else {
|
|
736
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
737
|
-
}
|
|
738
|
-
} else {
|
|
739
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
} else {
|
|
743
|
-
console.log("\n No agent instructions template found — skipping integration.\n");
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function printInstructionsFallback(agentMdPath, selectedAgents) {
|
|
748
|
-
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
749
|
-
const docTarget = hasNonClaude
|
|
750
|
-
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
751
|
-
: "your CLAUDE.md";
|
|
752
|
-
|
|
753
|
-
console.log(`
|
|
754
|
-
Agent instructions saved to: ${path.relative(process.cwd(), agentMdPath)}
|
|
755
|
-
|
|
756
|
-
Paste into ${docTarget}, or point your agent at the file
|
|
757
|
-
and ask it to integrate the instructions.
|
|
758
|
-
`);
|
|
759
652
|
}
|
|
760
653
|
|
|
761
654
|
// --- Status command ----------------------------------------------------------
|
|
@@ -823,9 +716,10 @@ async function runUpdate() {
|
|
|
823
716
|
const scriptsDir = path.join(process.env.HOME, ".config", "fathom-mcp", "scripts");
|
|
824
717
|
const copiedScripts = copyScripts(scriptsDir);
|
|
825
718
|
|
|
826
|
-
// Ensure SessionStart
|
|
719
|
+
// Ensure SessionStart hooks are registered for agents that support hooks
|
|
827
720
|
// Detect by config agents field or directory presence (older configs may lack agents)
|
|
828
721
|
const agents = found.config.agents || [];
|
|
722
|
+
const instructionsCmd = "bash ~/.config/fathom-mcp/scripts/fathom-instructions.sh";
|
|
829
723
|
const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
|
|
830
724
|
const registeredHooks = [];
|
|
831
725
|
|
|
@@ -836,6 +730,7 @@ async function runUpdate() {
|
|
|
836
730
|
const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
|
|
837
731
|
const settings = readJsonFile(settingsPath) || {};
|
|
838
732
|
let changed = ensurePermissions(settings);
|
|
733
|
+
changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000) || changed;
|
|
839
734
|
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
840
735
|
if (changed) {
|
|
841
736
|
writeJsonFile(settingsPath, settings);
|
|
@@ -849,7 +744,9 @@ async function runUpdate() {
|
|
|
849
744
|
if (hasGemini) {
|
|
850
745
|
const settingsPath = path.join(projectDir, ".gemini", "settings.json");
|
|
851
746
|
const settings = readJsonFile(settingsPath) || {};
|
|
852
|
-
|
|
747
|
+
let changed = ensureHook(settings, "SessionStart", instructionsCmd, 5000);
|
|
748
|
+
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
749
|
+
if (changed) {
|
|
853
750
|
writeJsonFile(settingsPath, settings);
|
|
854
751
|
registeredHooks.push("Gemini CLI → .gemini/settings.json");
|
|
855
752
|
}
|
package/src/index.js
CHANGED
|
@@ -253,77 +253,6 @@ const tools = [
|
|
|
253
253
|
},
|
|
254
254
|
];
|
|
255
255
|
|
|
256
|
-
// --- Telegram tools (primary agent only) -------------------------------------
|
|
257
|
-
|
|
258
|
-
const telegramTools = [
|
|
259
|
-
{
|
|
260
|
-
name: "fathom_telegram_contacts",
|
|
261
|
-
description:
|
|
262
|
-
"List Telegram contacts who have messaged. Returns name, username, chat_id, " +
|
|
263
|
-
"last_message time, and unread count.",
|
|
264
|
-
inputSchema: { type: "object", properties: {} },
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: "fathom_telegram_read",
|
|
268
|
-
description:
|
|
269
|
-
"Read recent Telegram messages from a contact. Same windowing as fathom_room_read — " +
|
|
270
|
-
"anchored to latest message, default 60 minutes. Use start to look further back.",
|
|
271
|
-
inputSchema: {
|
|
272
|
-
type: "object",
|
|
273
|
-
properties: {
|
|
274
|
-
contact: { type: "string", description: "Contact name, @username, or chat_id" },
|
|
275
|
-
minutes: { type: "number", description: "Window duration in minutes. Default: 60." },
|
|
276
|
-
start: { type: "number", description: "Offset in minutes from latest message. Default: 0." },
|
|
277
|
-
mark_read: { type: "boolean", description: "Mark messages as read. Default: true." },
|
|
278
|
-
},
|
|
279
|
-
required: ["contact"],
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: "fathom_telegram_send",
|
|
284
|
-
description:
|
|
285
|
-
"Send a Telegram message to a contact via the persistent Telethon client.",
|
|
286
|
-
inputSchema: {
|
|
287
|
-
type: "object",
|
|
288
|
-
properties: {
|
|
289
|
-
contact: { type: "string", description: "Contact name, @username, or chat_id" },
|
|
290
|
-
message: { type: "string", description: "Message text to send" },
|
|
291
|
-
},
|
|
292
|
-
required: ["contact", "message"],
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: "fathom_telegram_image",
|
|
297
|
-
description:
|
|
298
|
-
"Read a Telegram message's attached image and return it as base64 so Claude can perceive it. " +
|
|
299
|
-
"Use after fathom_telegram_read shows a message has media: true. " +
|
|
300
|
-
"Supports jpg, jpeg, png, gif, webp. Max 5MB.",
|
|
301
|
-
inputSchema: {
|
|
302
|
-
type: "object",
|
|
303
|
-
properties: {
|
|
304
|
-
message_id: { type: "number", description: "The message ID from fathom_telegram_read results" },
|
|
305
|
-
},
|
|
306
|
-
required: ["message_id"],
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
name: "fathom_telegram_send_image",
|
|
311
|
-
description:
|
|
312
|
-
"Send an image to a Telegram contact via the persistent Telethon client. " +
|
|
313
|
-
"Provide an absolute file path to a local image. Optionally include a caption. " +
|
|
314
|
-
"Contact can be a name, @username, or chat_id number.",
|
|
315
|
-
inputSchema: {
|
|
316
|
-
type: "object",
|
|
317
|
-
properties: {
|
|
318
|
-
contact: { type: "string", description: "Contact name, @username, or chat_id" },
|
|
319
|
-
file_path: { type: "string", description: "Absolute path to the image file to send" },
|
|
320
|
-
caption: { type: "string", description: "Optional caption text for the image (max 1024 chars)" },
|
|
321
|
-
},
|
|
322
|
-
required: ["contact", "file_path"],
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
];
|
|
326
|
-
|
|
327
256
|
// --- Policy evaluation tool (permission-prompt-tool for stream-json) ---------
|
|
328
257
|
|
|
329
258
|
const policyTools = [
|
|
@@ -362,7 +291,6 @@ const policyTools = [
|
|
|
362
291
|
|
|
363
292
|
// --- Primary-agent-only tools (reserved for future use) ----------------------
|
|
364
293
|
|
|
365
|
-
const primaryAgentTools = [];
|
|
366
294
|
|
|
367
295
|
// --- Policy evaluation -------------------------------------------------------
|
|
368
296
|
|
|
@@ -433,17 +361,7 @@ const server = new Server(
|
|
|
433
361
|
);
|
|
434
362
|
|
|
435
363
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
436
|
-
|
|
437
|
-
let showTelegram = false;
|
|
438
|
-
try {
|
|
439
|
-
const settings = await client.getSettings();
|
|
440
|
-
const primaryAgent = settings.default_workspace;
|
|
441
|
-
const wsType = settings.workspaces?.[config.workspace]?.type;
|
|
442
|
-
showTelegram = config.workspace === primaryAgent || wsType === 'human';
|
|
443
|
-
} catch {
|
|
444
|
-
// If settings unavailable, hide telegram tools
|
|
445
|
-
}
|
|
446
|
-
const allTools = [...tools, ...policyTools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
364
|
+
const allTools = [...tools, ...policyTools];
|
|
447
365
|
return { tools: allTools };
|
|
448
366
|
});
|
|
449
367
|
|
|
@@ -553,99 +471,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
553
471
|
break;
|
|
554
472
|
}
|
|
555
473
|
|
|
556
|
-
// --- Telegram ---
|
|
557
|
-
case "fathom_telegram_contacts":
|
|
558
|
-
result = await client.telegramContacts(config.workspace);
|
|
559
|
-
break;
|
|
560
|
-
case "fathom_telegram_read": {
|
|
561
|
-
// Resolve contact name to chat_id via contacts list
|
|
562
|
-
const contacts = await client.telegramContacts(config.workspace);
|
|
563
|
-
const contactList = contacts?.contacts || [];
|
|
564
|
-
const contactArg = (args.contact || "").trim();
|
|
565
|
-
let chatId = parseInt(contactArg, 10);
|
|
566
|
-
if (isNaN(chatId)) {
|
|
567
|
-
const lower = contactArg.toLowerCase().replace(/^@/, "");
|
|
568
|
-
const match = contactList.find(c =>
|
|
569
|
-
(c.username || "").toLowerCase() === lower ||
|
|
570
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
571
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
572
|
-
);
|
|
573
|
-
chatId = match ? match.chat_id : null;
|
|
574
|
-
}
|
|
575
|
-
if (!chatId) {
|
|
576
|
-
result = { error: `Contact not found: ${contactArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
577
|
-
} else {
|
|
578
|
-
result = await client.telegramRead(
|
|
579
|
-
chatId, args.minutes, args.start,
|
|
580
|
-
args.mark_read !== false ? config.workspace : undefined,
|
|
581
|
-
args.mark_read,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
case "fathom_telegram_send": {
|
|
587
|
-
const sendContacts = await client.telegramContacts(config.workspace);
|
|
588
|
-
const sendList = sendContacts?.contacts || [];
|
|
589
|
-
const sendArg = (args.contact || "").trim();
|
|
590
|
-
let sendChatId = parseInt(sendArg, 10);
|
|
591
|
-
if (isNaN(sendChatId)) {
|
|
592
|
-
const lower = sendArg.toLowerCase().replace(/^@/, "");
|
|
593
|
-
const match = sendList.find(c =>
|
|
594
|
-
(c.username || "").toLowerCase() === lower ||
|
|
595
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
596
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
597
|
-
);
|
|
598
|
-
sendChatId = match ? match.chat_id : null;
|
|
599
|
-
}
|
|
600
|
-
if (!sendChatId) {
|
|
601
|
-
result = { error: `Contact not found: ${sendArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
602
|
-
} else {
|
|
603
|
-
result = await client.telegramSend(sendChatId, args.message);
|
|
604
|
-
}
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
case "fathom_telegram_image": {
|
|
608
|
-
const msgId = args.message_id;
|
|
609
|
-
if (!msgId) {
|
|
610
|
-
result = { error: "message_id is required" };
|
|
611
|
-
} else {
|
|
612
|
-
// Check local WebSocket cache first (avoids HTTP round-trip for pushed images)
|
|
613
|
-
const cached = wsConn?.getCachedImage(msgId);
|
|
614
|
-
if (cached) {
|
|
615
|
-
result = { _image: true, data: cached.data, mimeType: cached.mimeType };
|
|
616
|
-
} else {
|
|
617
|
-
result = await client.telegramImage(msgId);
|
|
618
|
-
if (result?.data && result?.mimeType) {
|
|
619
|
-
result = { _image: true, data: result.data, mimeType: result.mimeType };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
case "fathom_telegram_send_image": {
|
|
626
|
-
const imgContactArg = args.contact;
|
|
627
|
-
if (!imgContactArg) { result = { error: "contact is required" }; break; }
|
|
628
|
-
if (!args.file_path) { result = { error: "file_path is required" }; break; }
|
|
629
|
-
|
|
630
|
-
const imgContacts = await client.telegramContacts(config.workspace);
|
|
631
|
-
const imgList = imgContacts?.contacts || [];
|
|
632
|
-
let imgChatId = parseInt(imgContactArg, 10);
|
|
633
|
-
if (isNaN(imgChatId)) {
|
|
634
|
-
const lower = imgContactArg.toLowerCase().replace(/^@/, "");
|
|
635
|
-
const match = imgList.find(c =>
|
|
636
|
-
(c.username || "").toLowerCase() === lower ||
|
|
637
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
638
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
639
|
-
);
|
|
640
|
-
imgChatId = match ? match.chat_id : null;
|
|
641
|
-
}
|
|
642
|
-
if (!imgChatId) {
|
|
643
|
-
result = { error: `Contact not found: ${imgContactArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
644
|
-
} else {
|
|
645
|
-
result = await client.telegramSendImage(imgChatId, args.file_path, args.caption);
|
|
646
|
-
}
|
|
647
|
-
break;
|
|
648
|
-
}
|
|
649
474
|
// --- Policy evaluation (permission-prompt-tool for stream-json agents) ---
|
|
650
475
|
case "policy_evaluate": {
|
|
651
476
|
result = await evaluatePermission(args.tool_name, args.input || {});
|
package/src/server-client.js
CHANGED
|
@@ -228,46 +228,6 @@ export function createClient(config) {
|
|
|
228
228
|
return request("POST", `/api/workspaces/${encodeURIComponent(ws)}/heartbeat`, { body });
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// --- Telegram --------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
async function telegramContacts(ws) {
|
|
234
|
-
return request("GET", "/api/telegram/contacts", {
|
|
235
|
-
params: { workspace: ws },
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function telegramRead(chatId, minutes, start, ws, markRead) {
|
|
240
|
-
return request("GET", `/api/telegram/messages/${chatId}`, {
|
|
241
|
-
params: { minutes, start, workspace: ws, mark_read: markRead },
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function telegramSend(chatId, message) {
|
|
246
|
-
return request("POST", `/api/telegram/send/${chatId}`, {
|
|
247
|
-
body: { message },
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function telegramImage(messageId) {
|
|
252
|
-
return request("GET", `/api/telegram/image/${messageId}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function telegramSendImage(chatId, filePath, caption) {
|
|
256
|
-
return request("POST", `/api/telegram/send-image/${chatId}`, {
|
|
257
|
-
body: { file_path: filePath, caption: caption || "" },
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function telegramSendVoice(chatId, filePath, caption) {
|
|
262
|
-
return request("POST", `/api/telegram/send-voice/${chatId}`, {
|
|
263
|
-
body: { file_path: filePath, caption: caption || "" },
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function telegramStatus() {
|
|
268
|
-
return request("GET", "/api/telegram/status");
|
|
269
|
-
}
|
|
270
|
-
|
|
271
231
|
// --- TTS ------------------------------------------------------------------
|
|
272
232
|
|
|
273
233
|
async function speak({ text, file, speed, play, format } = {}) {
|
|
@@ -333,13 +293,6 @@ export function createClient(config) {
|
|
|
333
293
|
deleteRoutine,
|
|
334
294
|
fireRoutine,
|
|
335
295
|
heartbeat,
|
|
336
|
-
telegramContacts,
|
|
337
|
-
telegramRead,
|
|
338
|
-
telegramSend,
|
|
339
|
-
telegramImage,
|
|
340
|
-
telegramSendImage,
|
|
341
|
-
telegramSendVoice,
|
|
342
|
-
telegramStatus,
|
|
343
296
|
speak,
|
|
344
297
|
voiceReply,
|
|
345
298
|
getSettings,
|
package/src/ws-connection.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* WebSocket push channel — receives server-pushed messages and handles them locally.
|
|
3
3
|
*
|
|
4
4
|
* Connects to fathom-server's /ws/agent/{workspace} endpoint. Receives:
|
|
5
|
-
* - image → cache base64 data to .fathom/telegram-cache/
|
|
6
5
|
* - ping → respond with pong
|
|
7
6
|
*
|
|
8
7
|
* Stream-json agents handle inject/ping_fire via subprocess stdin — the server
|
|
@@ -11,19 +10,15 @@
|
|
|
11
10
|
* Auto-reconnects with exponential backoff (1s → 60s cap).
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
import os from "os";
|
|
16
|
-
import path from "path";
|
|
17
13
|
import WebSocket from "ws";
|
|
18
14
|
|
|
19
15
|
const KEEPALIVE_INTERVAL_MS = 30_000;
|
|
20
16
|
const INITIAL_RECONNECT_MS = 1_000;
|
|
21
17
|
const MAX_RECONNECT_MS = 60_000;
|
|
22
|
-
const IMAGE_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
20
|
* @param {object} config — resolved config from config.js
|
|
26
|
-
* @returns {{
|
|
21
|
+
* @returns {{ close: () => void }}
|
|
27
22
|
*/
|
|
28
23
|
export function createWSConnection(config) {
|
|
29
24
|
const workspace = config.workspace;
|
|
@@ -37,17 +32,11 @@ export function createWSConnection(config) {
|
|
|
37
32
|
.replace(/^https:/, "wss:")
|
|
38
33
|
+ `/ws/agent/${encodeURIComponent(workspace)}`;
|
|
39
34
|
|
|
40
|
-
// Image cache directory
|
|
41
|
-
const cacheDir = path.join(os.homedir(), ".fathom", "telegram-cache");
|
|
42
|
-
|
|
43
35
|
let ws = null;
|
|
44
36
|
let reconnectDelay = INITIAL_RECONNECT_MS;
|
|
45
37
|
let keepaliveTimer = null;
|
|
46
38
|
let closed = false;
|
|
47
39
|
|
|
48
|
-
// Clean up old cached images on startup
|
|
49
|
-
cleanupImageCache();
|
|
50
|
-
|
|
51
40
|
connect();
|
|
52
41
|
|
|
53
42
|
function connect() {
|
|
@@ -98,10 +87,6 @@ export function createWSConnection(config) {
|
|
|
98
87
|
console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars) — handled by server subprocess`);
|
|
99
88
|
break;
|
|
100
89
|
|
|
101
|
-
case "image":
|
|
102
|
-
cacheImage(msg);
|
|
103
|
-
break;
|
|
104
|
-
|
|
105
90
|
case "ping":
|
|
106
91
|
safeSend({ type: "pong" });
|
|
107
92
|
break;
|
|
@@ -158,57 +143,6 @@ export function createWSConnection(config) {
|
|
|
158
143
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
159
144
|
}
|
|
160
145
|
|
|
161
|
-
// ── Image cache ─────────────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
function cacheImage(msg) {
|
|
164
|
-
if (!msg.message_id || !msg.data) return;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
168
|
-
const ext = (msg.filename || "").split(".").pop() || "jpg";
|
|
169
|
-
const filename = `${msg.message_id}.${ext}`;
|
|
170
|
-
const filePath = path.join(cacheDir, filename);
|
|
171
|
-
fs.writeFileSync(filePath, Buffer.from(msg.data, "base64"));
|
|
172
|
-
} catch {
|
|
173
|
-
// Cache write failure is non-fatal
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function getCachedImage(messageId) {
|
|
178
|
-
try {
|
|
179
|
-
const dir = fs.readdirSync(cacheDir);
|
|
180
|
-
const match = dir.find(f => f.startsWith(`${messageId}.`));
|
|
181
|
-
if (!match) return null;
|
|
182
|
-
|
|
183
|
-
const filePath = path.join(cacheDir, match);
|
|
184
|
-
const data = fs.readFileSync(filePath);
|
|
185
|
-
const ext = path.extname(match).slice(1).toLowerCase();
|
|
186
|
-
const mimeMap = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
|
|
187
|
-
return {
|
|
188
|
-
data: data.toString("base64"),
|
|
189
|
-
mimeType: mimeMap[ext] || "image/jpeg",
|
|
190
|
-
};
|
|
191
|
-
} catch {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function cleanupImageCache() {
|
|
197
|
-
try {
|
|
198
|
-
if (!fs.existsSync(cacheDir)) return;
|
|
199
|
-
const now = Date.now();
|
|
200
|
-
for (const file of fs.readdirSync(cacheDir)) {
|
|
201
|
-
const filePath = path.join(cacheDir, file);
|
|
202
|
-
const stat = fs.statSync(filePath);
|
|
203
|
-
if (now - stat.mtimeMs > IMAGE_CACHE_MAX_AGE_MS) {
|
|
204
|
-
fs.unlinkSync(filePath);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
// Cleanup failure is non-fatal
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
146
|
function close() {
|
|
213
147
|
closed = true;
|
|
214
148
|
stopKeepalive();
|
|
@@ -218,5 +152,5 @@ export function createWSConnection(config) {
|
|
|
218
152
|
}
|
|
219
153
|
}
|
|
220
154
|
|
|
221
|
-
return {
|
|
155
|
+
return { close };
|
|
222
156
|
}
|