fathom-mcp 0.4.8 → 0.4.10

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.8",
3
+ "version": "0.4.10",
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
@@ -91,6 +91,25 @@ function appendToGitignore(dir, patterns) {
91
91
  }
92
92
  }
93
93
 
94
+ /**
95
+ * Idempotently register a hook in a settings object.
96
+ * Works for both Claude Code and Gemini CLI (same JSON structure).
97
+ * Returns true if a new hook was added, false if already present.
98
+ */
99
+ function ensureHook(settings, eventName, command, timeout) {
100
+ const existing = settings.hooks?.[eventName] || [];
101
+ const alreadyRegistered = existing.some((entry) =>
102
+ entry.hooks?.some((h) => h.command === command)
103
+ );
104
+ if (alreadyRegistered) return false;
105
+ if (!settings.hooks) settings.hooks = {};
106
+ settings.hooks[eventName] = [
107
+ ...existing,
108
+ { hooks: [{ type: "command", command, timeout }] },
109
+ ];
110
+ return true;
111
+ }
112
+
94
113
  function copyScripts(targetDir) {
95
114
  fs.mkdirSync(targetDir, { recursive: true });
96
115
  try {
@@ -446,17 +465,19 @@ async function runInit(flags = {}) {
446
465
  ? (nonInteractive ? "vault" : await ask(rl, " Vault subdirectory", "vault"))
447
466
  : "vault";
448
467
 
449
- // 9. Hooks — only ask if Claude Code is selected
468
+ // 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Gemini CLI)
450
469
  const hasClaude = selectedAgents.includes("claude-code");
470
+ const hasGemini = selectedAgents.includes("gemini");
471
+ const hasHookAgent = hasClaude || hasGemini;
451
472
  let enableRecallHook = false;
452
473
  let enablePrecompactHook = false;
453
- if (hasClaude) {
474
+ if (hasHookAgent) {
454
475
  if (nonInteractive) {
455
476
  enableRecallHook = true;
456
477
  enablePrecompactHook = true;
457
478
  } else {
458
479
  console.log();
459
- enableRecallHook = await askYesNo(rl, " Enable vault recall on every message (UserPromptSubmit)?", true);
480
+ enableRecallHook = await askYesNo(rl, " Enable vault recall on every message?", true);
460
481
  enablePrecompactHook = await askYesNo(rl, " Enable PreCompact vault snapshot hook?", true);
461
482
  }
462
483
  }
@@ -531,60 +552,34 @@ async function runInit(flags = {}) {
531
552
  console.log(` ✓ ${result}`);
532
553
  }
533
554
 
534
- // Claude Code hooks only if claude-code is selected
535
- if (hasClaude) {
536
- const claudeSettingsPath = path.join(cwd, ".claude", "settings.local.json");
537
- const claudeSettings = readJsonFile(claudeSettingsPath) || {};
538
-
539
- const hooks = {};
540
-
541
- // SessionStart — always registered (version check should always be on)
542
- hooks["SessionStart"] = [
543
- ...(claudeSettings.hooks?.["SessionStart"] || []),
544
- ];
545
- const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
546
- const hasFathomSessionStart = hooks["SessionStart"].some((entry) =>
547
- entry.hooks?.some((h) => h.command === sessionStartCmd)
548
- );
549
- if (!hasFathomSessionStart) {
550
- hooks["SessionStart"].push({
551
- hooks: [{ type: "command", command: sessionStartCmd, timeout: 10000 }],
552
- });
553
- }
555
+ // Hook scripts (shared across agents)
556
+ const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
557
+ const recallCmd = "bash .fathom/scripts/fathom-recall.sh";
558
+ const precompactCmd = "bash .fathom/scripts/fathom-precompact.sh";
554
559
 
555
- if (enableRecallHook) {
556
- hooks["UserPromptSubmit"] = [
557
- ...(claudeSettings.hooks?.["UserPromptSubmit"] || []),
558
- ];
559
- const recallCmd = "bash .fathom/scripts/fathom-recall.sh";
560
- const hasFathomRecall = hooks["UserPromptSubmit"].some((entry) =>
561
- entry.hooks?.some((h) => h.command === recallCmd)
562
- );
563
- if (!hasFathomRecall) {
564
- hooks["UserPromptSubmit"].push({
565
- hooks: [{ type: "command", command: recallCmd, timeout: 10000 }],
566
- });
567
- }
568
- }
569
- if (enablePrecompactHook) {
570
- hooks["PreCompact"] = [
571
- ...(claudeSettings.hooks?.["PreCompact"] || []),
572
- ];
573
- const precompactCmd = "bash .fathom/scripts/fathom-precompact.sh";
574
- const hasFathomPrecompact = hooks["PreCompact"].some((entry) =>
575
- entry.hooks?.some((h) => h.command === precompactCmd)
576
- );
577
- if (!hasFathomPrecompact) {
578
- hooks["PreCompact"].push({
579
- hooks: [{ type: "command", command: precompactCmd, timeout: 30000 }],
580
- });
581
- }
560
+ // Claude Code hooks
561
+ if (hasClaude) {
562
+ const settingsPath = path.join(cwd, ".claude", "settings.local.json");
563
+ const settings = readJsonFile(settingsPath) || {};
564
+ let changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000);
565
+ if (enableRecallHook) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 10000) || changed;
566
+ if (enablePrecompactHook) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
567
+ if (changed) {
568
+ writeJsonFile(settingsPath, settings);
569
+ console.log(".claude/settings.local.json (hooks)");
582
570
  }
571
+ }
583
572
 
584
- if (Object.keys(hooks).length > 0) {
585
- claudeSettings.hooks = { ...(claudeSettings.hooks || {}), ...hooks };
586
- writeJsonFile(claudeSettingsPath, claudeSettings);
587
- console.log(" ✓ .claude/settings.local.json (hooks)");
573
+ // Gemini CLI hooks
574
+ if (hasGemini) {
575
+ const settingsPath = path.join(cwd, ".gemini", "settings.json");
576
+ const settings = readJsonFile(settingsPath) || {};
577
+ let changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000);
578
+ if (enableRecallHook) changed = ensureHook(settings, "BeforeAgent", recallCmd, 10000) || changed;
579
+ if (enablePrecompactHook) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
580
+ if (changed) {
581
+ writeJsonFile(settingsPath, settings);
582
+ console.log(" ✓ .gemini/settings.json (hooks)");
588
583
  }
589
584
  }
590
585
 
@@ -784,27 +779,33 @@ async function runUpdate() {
784
779
  fs.mkdirSync(fathomDir, { recursive: true });
785
780
  fs.writeFileSync(path.join(fathomDir, "version"), packageVersion + "\n");
786
781
 
787
- // Ensure SessionStart hook is registered for Claude Code workspaces
788
- // Detect by .claude/ dir (older configs may not have agents field)
789
- const hasClaude = (found.config.agents || []).includes("claude-code")
782
+ // Ensure SessionStart hook is registered for agents that support hooks
783
+ // Detect by config agents field or directory presence (older configs may lack agents)
784
+ const agents = found.config.agents || [];
785
+ const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
786
+ const registeredHooks = [];
787
+
788
+ // Claude Code
789
+ const hasClaude = agents.includes("claude-code")
790
790
  || fs.existsSync(path.join(projectDir, ".claude"));
791
- let hooksUpdated = false;
792
791
  if (hasClaude) {
793
- const claudeSettingsPath = path.join(projectDir, ".claude", "settings.local.json");
794
- const claudeSettings = readJsonFile(claudeSettingsPath) || {};
795
- const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
796
- const existingHooks = claudeSettings.hooks?.["SessionStart"] || [];
797
- const hasSessionStart = existingHooks.some((entry) =>
798
- entry.hooks?.some((h) => h.command === sessionStartCmd)
799
- );
800
- if (!hasSessionStart) {
801
- claudeSettings.hooks = claudeSettings.hooks || {};
802
- claudeSettings.hooks["SessionStart"] = [
803
- ...existingHooks,
804
- { hooks: [{ type: "command", command: sessionStartCmd, timeout: 10000 }] },
805
- ];
806
- writeJsonFile(claudeSettingsPath, claudeSettings);
807
- hooksUpdated = true;
792
+ const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
793
+ const settings = readJsonFile(settingsPath) || {};
794
+ if (ensureHook(settings, "SessionStart", sessionStartCmd, 10000)) {
795
+ writeJsonFile(settingsPath, settings);
796
+ registeredHooks.push("Claude Code .claude/settings.local.json");
797
+ }
798
+ }
799
+
800
+ // Gemini CLI
801
+ const hasGemini = agents.includes("gemini")
802
+ || fs.existsSync(path.join(projectDir, ".gemini"));
803
+ if (hasGemini) {
804
+ const settingsPath = path.join(projectDir, ".gemini", "settings.json");
805
+ const settings = readJsonFile(settingsPath) || {};
806
+ if (ensureHook(settings, "SessionStart", sessionStartCmd, 10000)) {
807
+ writeJsonFile(settingsPath, settings);
808
+ registeredHooks.push("Gemini CLI → .gemini/settings.json");
808
809
  }
809
810
  }
810
811
 
@@ -817,9 +818,11 @@ async function runUpdate() {
817
818
  }
818
819
  }
819
820
 
820
- if (hooksUpdated) {
821
- console.log("\n Registered hooks:");
822
- console.log(" SessionStart fathom-sessionstart.sh (version check)");
821
+ if (registeredHooks.length > 0) {
822
+ console.log("\n Registered SessionStart hooks:");
823
+ for (const hook of registeredHooks) {
824
+ console.log(` ${hook}`);
825
+ }
823
826
  }
824
827
 
825
828
  console.log(`\n Version written to .fathom/version`);
package/src/index.js CHANGED
@@ -291,6 +291,55 @@ const tools = [
291
291
  required: ["workspace", "message"],
292
292
  },
293
293
  },
294
+ {
295
+ name: "fathom_routine_create",
296
+ description:
297
+ "Create a new ping routine for a workspace. Routines fire on an interval and inject " +
298
+ "context into the persistent session. Use single_fire for one-shot routines that " +
299
+ "auto-disable after firing once. All parameters are optional with sensible defaults.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ name: { type: "string", description: "Routine name. Default: 'New Routine'." },
304
+ enabled: { type: "boolean", description: "Start enabled. Default: false." },
305
+ interval_minutes: { type: "integer", description: "Minutes between pings. Default: 60.", minimum: 1 },
306
+ single_fire: { type: "boolean", description: "Auto-disable after firing once. Default: false." },
307
+ workspace: WORKSPACE_PROP,
308
+ context_sources: {
309
+ type: "object",
310
+ description: "What to inject on each ping.",
311
+ properties: {
312
+ time: { type: "boolean", description: "Include current time/date. Default: true." },
313
+ scripts: {
314
+ type: "array",
315
+ description: "Shell commands to run and inject output.",
316
+ items: {
317
+ type: "object",
318
+ properties: {
319
+ label: { type: "string" },
320
+ command: { type: "string" },
321
+ enabled: { type: "boolean" },
322
+ },
323
+ },
324
+ },
325
+ texts: {
326
+ type: "array",
327
+ description: "Static text blocks to inject.",
328
+ items: {
329
+ type: "object",
330
+ properties: {
331
+ label: { type: "string" },
332
+ content: { type: "string" },
333
+ enabled: { type: "boolean" },
334
+ },
335
+ },
336
+ },
337
+ },
338
+ },
339
+ },
340
+ required: [],
341
+ },
342
+ },
294
343
  ];
295
344
 
296
345
  // --- Vault routing by mode ---------------------------------------------------
@@ -480,6 +529,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
480
529
  case "fathom_send":
481
530
  result = await client.sendToWorkspace(args.workspace, args.message, config.workspace);
482
531
  break;
532
+ case "fathom_routine_create": {
533
+ const routineParams = {};
534
+ if (args.name != null) routineParams.name = args.name;
535
+ if (args.enabled != null) routineParams.enabled = args.enabled;
536
+ if (args.interval_minutes != null) routineParams.intervalMinutes = args.interval_minutes;
537
+ if (args.single_fire != null) routineParams.singleFire = args.single_fire;
538
+ if (args.context_sources != null) routineParams.contextSources = args.context_sources;
539
+ result = await client.createRoutine(routineParams, args.workspace || config.workspace);
540
+ break;
541
+ }
483
542
  default:
484
543
  result = { error: `Unknown tool: ${name}` };
485
544
  }
@@ -539,6 +598,13 @@ async function startupSync() {
539
598
  await client.pushFile(config.workspace, filePath, content);
540
599
  }
541
600
  }
601
+
602
+ // Delete server files that no longer exist locally (local is source of truth)
603
+ if (diff.deleted?.length) {
604
+ for (const filePath of diff.deleted) {
605
+ await client.deleteFile(config.workspace, filePath);
606
+ }
607
+ }
542
608
  } catch {
543
609
  // Sync failure is non-fatal — local vault is source of truth
544
610
  }
@@ -551,6 +617,7 @@ async function main() {
551
617
  vault: config._rawVault,
552
618
  description: config.description,
553
619
  agents: config.agents,
620
+ type: config.vaultMode,
554
621
  }).catch(() => {});
555
622
  }
556
623
 
@@ -190,6 +190,21 @@ export function createClient(config) {
190
190
  });
191
191
  }
192
192
 
193
+ // --- Activation / Routines -------------------------------------------------
194
+
195
+ async function createRoutine(params, ws) {
196
+ const body = {};
197
+ if (params.name != null) body.name = params.name;
198
+ if (params.enabled != null) body.enabled = params.enabled;
199
+ if (params.intervalMinutes != null) body.intervalMinutes = params.intervalMinutes;
200
+ if (params.singleFire != null) body.singleFire = params.singleFire;
201
+ if (params.contextSources != null) body.contextSources = params.contextSources;
202
+ return request("POST", "/api/activation/ping/routines", {
203
+ params: { workspace: ws },
204
+ body,
205
+ });
206
+ }
207
+
193
208
  // --- Auth ------------------------------------------------------------------
194
209
 
195
210
  async function getApiKey() {
@@ -229,6 +244,7 @@ export function createClient(config) {
229
244
  listFiles,
230
245
  pushFile,
231
246
  syncManifest,
247
+ createRoutine,
232
248
  getApiKey,
233
249
  healthCheck,
234
250
  };