fathom-mcp 0.4.11 → 0.4.13

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.4.11",
3
+ "version": "0.4.13",
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
@@ -129,7 +129,7 @@ function copyScripts(targetDir) {
129
129
  // --- Headless agent integration ----------------------------------------------
130
130
 
131
131
  const HEADLESS_CMDS = {
132
- "claude-code": (prompt) => ["claude", "-p", prompt],
132
+ "claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
133
133
  "codex": (prompt) => ["codex", "exec", prompt],
134
134
  "gemini": (prompt) => ["gemini", prompt],
135
135
  "opencode": (prompt) => ["opencode", "run", prompt],
@@ -679,12 +679,13 @@ async function runInit(flags = {}) {
679
679
  } else {
680
680
  if (cmdParts) {
681
681
  const [cmd, ...args] = cmdParts;
682
- const displayCmd = `${cmd} ${args[0]}${args.length > 1 ? " ..." : ""}`;
682
+ const flagArgs = args.slice(0, -1).join(" ");
683
+ const displayCmd = `${cmd} ${flagArgs} <prompt>`;
683
684
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
684
685
  console.log("\n" + "─".repeat(60));
685
686
  const integrate = await askYesNo(
686
687
  rl2,
687
- `\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n Proceed?`,
688
+ `\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?`,
688
689
  true,
689
690
  );
690
691
  rl2.close();
package/src/index.js CHANGED
@@ -209,8 +209,8 @@ const tools = [
209
209
  "Post a message to a shared room. Rooms are created implicitly on first post. " +
210
210
  "Use this for ambient, multilateral communication — unlike fathom_send (point-to-point DM), " +
211
211
  "room messages are visible to all participants. Responding is optional — use `<...>` for active silence. " +
212
- "Supports @workspace mentions (e.g. @fathom, @navier-stokes) — mentioned workspaces get the message " +
213
- "injected into their Claude session, same mechanism as fathom_send. Use @all to notify every workspace except sender.",
212
+ "Supports @workspace mentions (e.g. @fathom, @navier-stokes) — mentioned workspaces receive " +
213
+ "notifications in their mentions:{workspace} virtual room via fathom_room_list. Use @all to notify every workspace except sender.",
214
214
  inputSchema: {
215
215
  type: "object",
216
216
  properties: {
@@ -227,7 +227,8 @@ const tools = [
227
227
  "to the latest message. Default: 60 minutes before the latest message. Use start to look " +
228
228
  "further back. Example: minutes=15, start=120 returns 15 minutes of conversation starting " +
229
229
  "2 hours before the latest message. Response includes window metadata with has_older flag " +
230
- "for pseudo-pagination. Automatically marks the room as read for this workspace unless mark_read=false.",
230
+ "for pseudo-pagination. All rooms are persistent messages are never deleted on read. " +
231
+ "Automatically marks the room as read unless mark_read=false.",
231
232
  inputSchema: {
232
233
  type: "object",
233
234
  properties: {
@@ -244,7 +245,8 @@ const tools = [
244
245
  description:
245
246
  "List all rooms with activity summary — message count, last activity time, last sender, " +
246
247
  "description, and per-room unread_count for this workspace. Use to discover active rooms " +
247
- "and see which have new messages.",
248
+ "and see which have new messages. DM rooms (dm:a+b) are filtered by workspace param — " +
249
+ "only participants see them. Mention rooms (mentions:{workspace}) visible only to the target workspace.",
248
250
  inputSchema: {
249
251
  type: "object",
250
252
  properties: {},
@@ -279,9 +281,9 @@ const tools = [
279
281
  {
280
282
  name: "fathom_send",
281
283
  description:
282
- "Send a message to another workspace's agent instance — for cross-workspace coordination, " +
283
- "sharing findings, or requesting action. Use fathom_workspaces first to discover valid " +
284
- "targets. The target agent sees: 'Message from workspace ({from}): {message}'",
284
+ "Send a message to another workspace's agent instance — stored in a shared dm:a+b room " +
285
+ "visible to both participants. Use fathom_workspaces first to discover valid targets. " +
286
+ "DMs are persistent and appear in both participants' room lists via fathom_room_list.",
285
287
  inputSchema: {
286
288
  type: "object",
287
289
  properties: {
@@ -340,6 +342,99 @@ const tools = [
340
342
  required: [],
341
343
  },
342
344
  },
345
+ {
346
+ name: "fathom_routine_list",
347
+ description:
348
+ "List all ping routines for a workspace with their status, intervals, " +
349
+ "enabled state, and next fire time. Use this to see what routines exist " +
350
+ "before updating or deleting them.",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ workspace: WORKSPACE_PROP,
355
+ },
356
+ required: [],
357
+ },
358
+ },
359
+ {
360
+ name: "fathom_routine_update",
361
+ description:
362
+ "Update an existing ping routine. Only provided fields are changed — " +
363
+ "omitted fields keep their current values. Use fathom_routine_list first " +
364
+ "to find the routine_id.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {
368
+ routine_id: { type: "string", description: "The routine ID to update. Use fathom_routine_list to find IDs." },
369
+ name: { type: "string", description: "New routine name." },
370
+ enabled: { type: "boolean", description: "Enable or disable the routine." },
371
+ interval_minutes: { type: "integer", description: "New interval in minutes.", minimum: 1 },
372
+ single_fire: { type: "boolean", description: "Auto-disable after firing once." },
373
+ workspace: WORKSPACE_PROP,
374
+ context_sources: {
375
+ type: "object",
376
+ description: "What to inject on each ping.",
377
+ properties: {
378
+ time: { type: "boolean", description: "Include current time/date. Default: true." },
379
+ scripts: {
380
+ type: "array",
381
+ description: "Shell commands to run and inject output.",
382
+ items: {
383
+ type: "object",
384
+ properties: {
385
+ label: { type: "string" },
386
+ command: { type: "string" },
387
+ enabled: { type: "boolean" },
388
+ },
389
+ },
390
+ },
391
+ texts: {
392
+ type: "array",
393
+ description: "Static text blocks to inject.",
394
+ items: {
395
+ type: "object",
396
+ properties: {
397
+ label: { type: "string" },
398
+ content: { type: "string" },
399
+ enabled: { type: "boolean" },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ },
405
+ },
406
+ required: ["routine_id"],
407
+ },
408
+ },
409
+ {
410
+ name: "fathom_routine_delete",
411
+ description:
412
+ "Delete a ping routine permanently. Use fathom_routine_list first to find " +
413
+ "the routine_id. This cannot be undone — create a new routine if needed.",
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {
417
+ routine_id: { type: "string", description: "The routine ID to delete." },
418
+ workspace: WORKSPACE_PROP,
419
+ },
420
+ required: ["routine_id"],
421
+ },
422
+ },
423
+ {
424
+ name: "fathom_routine_fire",
425
+ description:
426
+ "Fire a ping routine immediately, regardless of its schedule. Non-blocking — " +
427
+ "returns immediately while the routine fires in the background. The routine's " +
428
+ "next scheduled fire time is not affected.",
429
+ inputSchema: {
430
+ type: "object",
431
+ properties: {
432
+ routine_id: { type: "string", description: "The routine ID to fire." },
433
+ workspace: WORKSPACE_PROP,
434
+ },
435
+ required: ["routine_id"],
436
+ },
437
+ },
343
438
  ];
344
439
 
345
440
  // --- Vault routing by mode ---------------------------------------------------
@@ -377,6 +472,47 @@ function resolveVault(args) {
377
472
  }
378
473
  }
379
474
 
475
+ // --- Telegram tools (primary agent only) -------------------------------------
476
+
477
+ const telegramTools = [
478
+ {
479
+ name: "fathom_telegram_contacts",
480
+ description:
481
+ "List Telegram contacts who have messaged. Returns name, username, chat_id, " +
482
+ "last_message time, and unread count.",
483
+ inputSchema: { type: "object", properties: {} },
484
+ },
485
+ {
486
+ name: "fathom_telegram_read",
487
+ description:
488
+ "Read recent Telegram messages from a contact. Same windowing as fathom_room_read — " +
489
+ "anchored to latest message, default 60 minutes. Use start to look further back.",
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ contact: { type: "string", description: "Contact name, @username, or chat_id" },
494
+ minutes: { type: "number", description: "Window duration in minutes. Default: 60." },
495
+ start: { type: "number", description: "Offset in minutes from latest message. Default: 0." },
496
+ mark_read: { type: "boolean", description: "Mark messages as read. Default: true." },
497
+ },
498
+ required: ["contact"],
499
+ },
500
+ },
501
+ {
502
+ name: "fathom_telegram_send",
503
+ description:
504
+ "Send a Telegram message to a contact via the persistent Telethon client.",
505
+ inputSchema: {
506
+ type: "object",
507
+ properties: {
508
+ contact: { type: "string", description: "Contact name, @username, or chat_id" },
509
+ message: { type: "string", description: "Message text to send" },
510
+ },
511
+ required: ["contact", "message"],
512
+ },
513
+ },
514
+ ];
515
+
380
516
  // --- Server setup & dispatch -------------------------------------------------
381
517
 
382
518
  const server = new Server(
@@ -384,7 +520,19 @@ const server = new Server(
384
520
  { capabilities: { tools: {} } },
385
521
  );
386
522
 
387
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
523
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
524
+ // Telegram tools only visible to the primary (default) workspace
525
+ let isPrimary = false;
526
+ try {
527
+ const settings = await client.getSettings();
528
+ const primaryAgent = settings.default_workspace;
529
+ isPrimary = config.workspace === primaryAgent;
530
+ } catch {
531
+ // If settings unavailable, hide telegram tools
532
+ }
533
+ const allTools = [...tools, ...(isPrimary ? telegramTools : [])];
534
+ return { tools: allTools };
535
+ });
388
536
 
389
537
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
390
538
  const { name, arguments: args } = request.params;
@@ -539,6 +687,76 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
539
687
  result = await client.createRoutine(routineParams, args.workspace || config.workspace);
540
688
  break;
541
689
  }
690
+ case "fathom_routine_list":
691
+ result = await client.listRoutines(args.workspace || config.workspace);
692
+ break;
693
+ case "fathom_routine_update": {
694
+ const updateParams = {};
695
+ if (args.name != null) updateParams.name = args.name;
696
+ if (args.enabled != null) updateParams.enabled = args.enabled;
697
+ if (args.interval_minutes != null) updateParams.intervalMinutes = args.interval_minutes;
698
+ if (args.single_fire != null) updateParams.singleFire = args.single_fire;
699
+ if (args.context_sources != null) updateParams.contextSources = args.context_sources;
700
+ result = await client.updateRoutine(args.routine_id, updateParams, args.workspace || config.workspace);
701
+ break;
702
+ }
703
+ case "fathom_routine_delete":
704
+ result = await client.deleteRoutine(args.routine_id, args.workspace || config.workspace);
705
+ break;
706
+ case "fathom_routine_fire":
707
+ result = await client.fireRoutine(args.routine_id, args.workspace || config.workspace);
708
+ break;
709
+ // --- Telegram ---
710
+ case "fathom_telegram_contacts":
711
+ result = await client.telegramContacts(config.workspace);
712
+ break;
713
+ case "fathom_telegram_read": {
714
+ // Resolve contact name to chat_id via contacts list
715
+ const contacts = await client.telegramContacts(config.workspace);
716
+ const contactList = contacts?.contacts || [];
717
+ const contactArg = (args.contact || "").trim();
718
+ let chatId = parseInt(contactArg, 10);
719
+ if (isNaN(chatId)) {
720
+ const lower = contactArg.toLowerCase().replace(/^@/, "");
721
+ const match = contactList.find(c =>
722
+ (c.username || "").toLowerCase() === lower ||
723
+ (c.first_name || "").toLowerCase() === lower ||
724
+ (c.first_name || "").toLowerCase().includes(lower)
725
+ );
726
+ chatId = match ? match.chat_id : null;
727
+ }
728
+ if (!chatId) {
729
+ result = { error: `Contact not found: ${contactArg}. Use fathom_telegram_contacts to list known contacts.` };
730
+ } else {
731
+ result = await client.telegramRead(
732
+ chatId, args.minutes, args.start,
733
+ args.mark_read !== false ? config.workspace : undefined,
734
+ args.mark_read,
735
+ );
736
+ }
737
+ break;
738
+ }
739
+ case "fathom_telegram_send": {
740
+ const sendContacts = await client.telegramContacts(config.workspace);
741
+ const sendList = sendContacts?.contacts || [];
742
+ const sendArg = (args.contact || "").trim();
743
+ let sendChatId = parseInt(sendArg, 10);
744
+ if (isNaN(sendChatId)) {
745
+ const lower = sendArg.toLowerCase().replace(/^@/, "");
746
+ const match = sendList.find(c =>
747
+ (c.username || "").toLowerCase() === lower ||
748
+ (c.first_name || "").toLowerCase() === lower ||
749
+ (c.first_name || "").toLowerCase().includes(lower)
750
+ );
751
+ sendChatId = match ? match.chat_id : null;
752
+ }
753
+ if (!sendChatId) {
754
+ result = { error: `Contact not found: ${sendArg}. Use fathom_telegram_contacts to list known contacts.` };
755
+ } else {
756
+ result = await client.telegramSend(sendChatId, args.message);
757
+ }
758
+ break;
759
+ }
542
760
  default:
543
761
  result = { error: `Unknown tool: ${name}` };
544
762
  }
@@ -205,6 +205,37 @@ export function createClient(config) {
205
205
  });
206
206
  }
207
207
 
208
+ async function listRoutines(ws) {
209
+ return request("GET", "/api/activation/ping/routines", {
210
+ params: { workspace: ws },
211
+ });
212
+ }
213
+
214
+ async function updateRoutine(routineId, params, ws) {
215
+ const body = {};
216
+ if (params.name != null) body.name = params.name;
217
+ if (params.enabled != null) body.enabled = params.enabled;
218
+ if (params.intervalMinutes != null) body.intervalMinutes = params.intervalMinutes;
219
+ if (params.singleFire != null) body.singleFire = params.singleFire;
220
+ if (params.contextSources != null) body.contextSources = params.contextSources;
221
+ return request("POST", `/api/activation/ping/routines/${encodeURIComponent(routineId)}`, {
222
+ params: { workspace: ws },
223
+ body,
224
+ });
225
+ }
226
+
227
+ async function deleteRoutine(routineId, ws) {
228
+ return request("DELETE", `/api/activation/ping/routines/${encodeURIComponent(routineId)}`, {
229
+ params: { workspace: ws },
230
+ });
231
+ }
232
+
233
+ async function fireRoutine(routineId, ws) {
234
+ return request("POST", `/api/activation/ping/routines/${encodeURIComponent(routineId)}/now`, {
235
+ params: { workspace: ws },
236
+ });
237
+ }
238
+
208
239
  // --- Heartbeat -------------------------------------------------------------
209
240
 
210
241
  async function heartbeat(ws, agent, vaultMode) {
@@ -214,6 +245,36 @@ export function createClient(config) {
214
245
  return request("POST", `/api/workspaces/${encodeURIComponent(ws)}/heartbeat`, { body });
215
246
  }
216
247
 
248
+ // --- Telegram --------------------------------------------------------------
249
+
250
+ async function telegramContacts(ws) {
251
+ return request("GET", "/api/telegram/contacts", {
252
+ params: { workspace: ws },
253
+ });
254
+ }
255
+
256
+ async function telegramRead(chatId, minutes, start, ws, markRead) {
257
+ return request("GET", `/api/telegram/messages/${chatId}`, {
258
+ params: { minutes, start, workspace: ws, mark_read: markRead },
259
+ });
260
+ }
261
+
262
+ async function telegramSend(chatId, message) {
263
+ return request("POST", `/api/telegram/send/${chatId}`, {
264
+ body: { message },
265
+ });
266
+ }
267
+
268
+ async function telegramStatus() {
269
+ return request("GET", "/api/telegram/status");
270
+ }
271
+
272
+ // --- Settings --------------------------------------------------------------
273
+
274
+ async function getSettings() {
275
+ return request("GET", "/api/settings");
276
+ }
277
+
217
278
  // --- Auth ------------------------------------------------------------------
218
279
 
219
280
  async function getApiKey() {
@@ -254,7 +315,16 @@ export function createClient(config) {
254
315
  pushFile,
255
316
  syncManifest,
256
317
  createRoutine,
318
+ listRoutines,
319
+ updateRoutine,
320
+ deleteRoutine,
321
+ fireRoutine,
257
322
  heartbeat,
323
+ telegramContacts,
324
+ telegramRead,
325
+ telegramSend,
326
+ telegramStatus,
327
+ getSettings,
258
328
  getApiKey,
259
329
  healthCheck,
260
330
  };