fathom-mcp 0.6.2 → 0.6.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fathom-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -154,19 +154,32 @@ 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");
157
+ // ---------------------------------------------------------------------------
158
+ // CLAUDE.md integration
159
+ // ---------------------------------------------------------------------------
160
+
161
+ const FATHOM_MD_MARKER_START = "<!-- BEGIN fathom-mcp -->";
162
+ const FATHOM_MD_MARKER_END = "<!-- END fathom-mcp -->";
163
+
164
+ function appendToClaudeMd(cwd, blob) {
165
+ const mdPath = path.join(cwd, "CLAUDE.md");
166
+ let content = "";
167
+ try { content = fs.readFileSync(mdPath, "utf-8"); } catch { /* new file */ }
168
+
169
+ const wrappedBlob = `${FATHOM_MD_MARKER_START}\n${blob}\n${FATHOM_MD_MARKER_END}`;
170
+
171
+ const markerRe = new RegExp(
172
+ `${FATHOM_MD_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${FATHOM_MD_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
173
+ );
174
+
175
+ if (markerRe.test(content)) {
176
+ content = content.replace(markerRe, wrappedBlob);
177
+ } else {
178
+ content = content.trimEnd() + "\n\n" + wrappedBlob + "\n";
179
+ }
180
+
181
+ fs.writeFileSync(mdPath, content, "utf-8");
182
+ return mdPath;
170
183
  }
171
184
 
172
185
  /**
@@ -182,29 +195,6 @@ function detectFathomServer() {
182
195
  }
183
196
  }
184
197
 
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
198
  // --- CLI flag parsing --------------------------------------------------------
209
199
 
210
200
  function parseFlags(argv) {
@@ -683,60 +673,31 @@ async function runInit(flags = {}) {
683
673
  console.log(`\n Non-interactive equivalent:\n ${parts.join(" ")}\n`);
684
674
  }
685
675
 
686
- // Auto-integrate agent instructions
687
- const agentMdPath = agentMdDest;
676
+ // Append agent instructions to CLAUDE.md
688
677
  let instructionsBlob = "";
689
678
  try {
690
- instructionsBlob = fs.readFileSync(agentMdPath, "utf-8");
691
- } catch { /* file wasn't created — use empty */ }
692
-
693
- const primaryAgent = selectedAgents[0];
679
+ instructionsBlob = fs.readFileSync(agentMdDest, "utf-8");
680
+ } catch { /* template wasn't created — skip */ }
694
681
 
695
682
  if (instructionsBlob) {
696
- const prompt = buildIntegrationPrompt(instructionsBlob);
697
- const cmdBuilder = AGENT_PROMPT_CMDS[primaryAgent];
698
-
699
683
  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
- }
684
+ const mdPath = appendToClaudeMd(cwd, instructionsBlob);
685
+ console.log(`\n Instructions written to ${path.relative(cwd, mdPath)}\n`);
712
686
  } 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
- }
687
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
688
+ console.log("\n" + "".repeat(60));
689
+ const integrate = await askYesNo(
690
+ rl2,
691
+ "\n Append Fathom instructions to CLAUDE.md?",
692
+ true,
693
+ );
694
+ rl2.close();
695
+
696
+ if (integrate) {
697
+ const mdPath = appendToClaudeMd(cwd, instructionsBlob);
698
+ console.log(`\n ✓ Instructions written to ${path.relative(cwd, mdPath)}\n`);
738
699
  } else {
739
- printInstructionsFallback(agentMdPath, selectedAgents);
700
+ printInstructionsFallback(agentMdDest, selectedAgents);
740
701
  }
741
702
  }
742
703
  } else {
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
- // Telegram tools only visible to primary agent workspace + human workspaces
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 || {});
@@ -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,
@@ -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 {{ getCachedImage: (messageId: number) => {data: string, mimeType: string} | null, close: () => void }}
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 { getCachedImage, close };
155
+ return { close };
222
156
  }