fathom-mcp 0.6.4 → 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.
@@ -1,303 +0,0 @@
1
- /**
2
- * HTTP client for fathom-server REST API.
3
- *
4
- * Handles search, rooms, workspaces, and access notifications.
5
- * Uses native fetch (Node 18+). Auth via Bearer token from .fathom.json.
6
- */
7
-
8
- /**
9
- * @param {object} config - Resolved config from config.js
10
- */
11
- export function createClient(config) {
12
- const baseUrl = config.server;
13
- const apiKey = config.apiKey;
14
- const workspace = config.workspace;
15
-
16
- /** Percent-encode each segment of a file path for safe URL interpolation. */
17
- function encodeFilePath(fp) {
18
- return fp.split("/").map(encodeURIComponent).join("/");
19
- }
20
-
21
- /**
22
- * Make an authenticated request to the server.
23
- * Returns parsed JSON on success, { error } on failure.
24
- */
25
- async function request(method, path, { params, body, timeout = 30000 } = {}) {
26
- const url = new URL(path, baseUrl);
27
- if (params) {
28
- for (const [k, v] of Object.entries(params)) {
29
- if (v != null) url.searchParams.set(k, String(v));
30
- }
31
- }
32
-
33
- const headers = { "Content-Type": "application/json" };
34
- if (apiKey) {
35
- headers["Authorization"] = `Bearer ${apiKey}`;
36
- }
37
-
38
- const controller = new AbortController();
39
- const timer = setTimeout(() => controller.abort(), timeout);
40
-
41
- try {
42
- const resp = await fetch(url.toString(), {
43
- method,
44
- headers,
45
- body: body ? JSON.stringify(body) : undefined,
46
- signal: controller.signal,
47
- });
48
-
49
- const data = await resp.json();
50
-
51
- if (!resp.ok) {
52
- return { error: data.error || `Server returned ${resp.status}` };
53
- }
54
-
55
- return data;
56
- } catch (e) {
57
- if (e.name === "AbortError") {
58
- return { error: `Request timed out after ${timeout / 1000}s` };
59
- }
60
- return { error: `Server unavailable: ${e.message}` };
61
- } finally {
62
- clearTimeout(timer);
63
- }
64
- }
65
-
66
- // --- Search ----------------------------------------------------------------
67
-
68
- async function search(query, { mode = "bm25", limit, ws } = {}) {
69
- return request("GET", "/api/search", {
70
- params: { q: query, mode, n: limit, workspace: ws || workspace },
71
- });
72
- }
73
-
74
- async function vsearch(query, { limit, ws } = {}) {
75
- return search(query, { mode: "vector", limit, ws });
76
- }
77
-
78
- async function hybridSearch(query, { limit, ws } = {}) {
79
- return search(query, { mode: "hybrid", limit, ws });
80
- }
81
-
82
- // --- Rooms -----------------------------------------------------------------
83
-
84
- async function roomPost(room, message, sender) {
85
- return request("POST", `/api/room/${encodeURIComponent(room)}`, {
86
- body: { message, sender },
87
- });
88
- }
89
-
90
- async function roomRead(room, minutes, start, ws, markRead) {
91
- return request("GET", `/api/room/${encodeURIComponent(room)}`, {
92
- params: { minutes, start, workspace: ws, mark_read: markRead },
93
- });
94
- }
95
-
96
- async function roomList(ws) {
97
- return request("GET", "/api/room/list", {
98
- params: { workspace: ws },
99
- });
100
- }
101
-
102
- async function roomMarkRead(room, ws) {
103
- return request("POST", `/api/room/${encodeURIComponent(room)}/read`, {
104
- body: { workspace: ws },
105
- });
106
- }
107
-
108
- async function roomDescribe(room, description) {
109
- return request("PUT", `/api/room/${encodeURIComponent(room)}/description`, {
110
- body: { description },
111
- });
112
- }
113
-
114
- // --- Direct messaging -------------------------------------------------------
115
-
116
- async function sendToWorkspace(target, message, from) {
117
- return request("POST", `/api/send/${encodeURIComponent(target)}`, {
118
- body: { message, from: from || workspace },
119
- });
120
- }
121
-
122
- // --- Workspaces ------------------------------------------------------------
123
-
124
- async function listWorkspaces() {
125
- return request("GET", "/api/workspaces/profiles");
126
- }
127
-
128
- async function registerWorkspace(name, projectPath, { vault, description, agents, type } = {}) {
129
- const body = { name, path: projectPath };
130
- if (vault) body.vault = vault;
131
- if (description) body.description = description;
132
- if (agents && agents.length > 0) body.agents = agents;
133
- if (type) body.type = type;
134
- return request("POST", "/api/workspaces", { body });
135
- }
136
-
137
- async function provisionWorkspace(name, projectPath, { vault, description, agents, type, execution, ssh } = {}) {
138
- const body = { name, path: projectPath };
139
- if (vault) body.vault = vault;
140
- if (description) body.description = description;
141
- if (agents && agents.length > 0) body.agents = agents;
142
- if (type) body.type = type;
143
- if (execution) body.execution = execution;
144
- if (ssh) body.ssh = ssh;
145
- return request("POST", "/api/workspaces/provision", { body });
146
- }
147
-
148
- // --- Access tracking -------------------------------------------------------
149
-
150
- async function notifyAccess(filePath, ws) {
151
- return request("POST", "/api/vault/access", {
152
- params: { workspace: ws || workspace },
153
- body: { path: filePath },
154
- });
155
- }
156
-
157
- // --- Activity-enriched listings (via server) --------------------------------
158
-
159
- async function vaultList(ws) {
160
- return request("GET", "/api/vault", {
161
- params: { workspace: ws || workspace },
162
- });
163
- }
164
-
165
- async function vaultFolder(folder, ws) {
166
- const folderPath = folder || "";
167
- return request("GET", `/api/vault/folder/${encodeFilePath(folderPath)}`, {
168
- params: { workspace: ws || workspace },
169
- });
170
- }
171
-
172
- // --- Activation / Routines -------------------------------------------------
173
-
174
- async function createRoutine(params, ws) {
175
- const body = {};
176
- if (params.name != null) body.name = params.name;
177
- if (params.enabled != null) body.enabled = params.enabled;
178
- if (params.intervalMinutes != null) body.intervalMinutes = params.intervalMinutes;
179
- if (params.singleFire != null) body.singleFire = params.singleFire;
180
- if (params.contextSources != null) body.contextSources = params.contextSources;
181
- if (params.schedule !== undefined) body.schedule = params.schedule;
182
- if (params.conditions != null) body.conditions = params.conditions;
183
- return request("POST", "/api/activation/ping/routines", {
184
- params: { workspace: ws },
185
- body,
186
- });
187
- }
188
-
189
- async function listRoutines(ws) {
190
- return request("GET", "/api/activation/ping/routines", {
191
- params: { workspace: ws },
192
- });
193
- }
194
-
195
- async function updateRoutine(routineId, 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
- if (params.schedule !== undefined) body.schedule = params.schedule;
203
- if (params.conditions != null) body.conditions = params.conditions;
204
- return request("POST", `/api/activation/ping/routines/${encodeURIComponent(routineId)}`, {
205
- params: { workspace: ws },
206
- body,
207
- });
208
- }
209
-
210
- async function deleteRoutine(routineId, ws) {
211
- return request("DELETE", `/api/activation/ping/routines/${encodeURIComponent(routineId)}`, {
212
- params: { workspace: ws },
213
- });
214
- }
215
-
216
- async function fireRoutine(routineId, ws) {
217
- return request("POST", `/api/activation/ping/routines/${encodeURIComponent(routineId)}/now`, {
218
- params: { workspace: ws },
219
- });
220
- }
221
-
222
- // --- Heartbeat -------------------------------------------------------------
223
-
224
- async function heartbeat(ws, agent, vaultMode) {
225
- const body = {};
226
- if (agent) body.agent = agent;
227
- if (vaultMode) body.vault_mode = vaultMode;
228
- return request("POST", `/api/workspaces/${encodeURIComponent(ws)}/heartbeat`, { body });
229
- }
230
-
231
- // --- TTS ------------------------------------------------------------------
232
-
233
- async function speak({ text, file, speed, play, format } = {}) {
234
- return request("POST", "/api/tts/speak", {
235
- body: { text, file, speed, play, format },
236
- timeout: 300000,
237
- });
238
- }
239
-
240
- async function voiceReply({ text, speed, workspace } = {}) {
241
- return request("POST", "/api/voice/reply", {
242
- body: { text, speed, workspace },
243
- timeout: 300000,
244
- });
245
- }
246
-
247
- // --- Settings --------------------------------------------------------------
248
-
249
- async function getSettings() {
250
- return request("GET", "/api/settings");
251
- }
252
-
253
- // --- Auth ------------------------------------------------------------------
254
-
255
- async function getApiKey() {
256
- return request("GET", "/api/auth/key");
257
- }
258
-
259
- async function rotateKey() {
260
- return request("POST", "/api/auth/keys/rotate");
261
- }
262
-
263
- async function healthCheck() {
264
- try {
265
- const resp = await fetch(`${baseUrl}/api/auth/status`, {
266
- signal: AbortSignal.timeout(5000),
267
- });
268
- return resp.ok;
269
- } catch {
270
- return false;
271
- }
272
- }
273
-
274
- return {
275
- search,
276
- vsearch,
277
- hybridSearch,
278
- roomPost,
279
- roomRead,
280
- roomList,
281
- roomMarkRead,
282
- roomDescribe,
283
- sendToWorkspace,
284
- listWorkspaces,
285
- registerWorkspace,
286
- provisionWorkspace,
287
- notifyAccess,
288
- vaultList,
289
- vaultFolder,
290
- createRoutine,
291
- listRoutines,
292
- updateRoutine,
293
- deleteRoutine,
294
- fireRoutine,
295
- heartbeat,
296
- speak,
297
- voiceReply,
298
- getSettings,
299
- getApiKey,
300
- rotateKey,
301
- healthCheck,
302
- };
303
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * WebSocket push channel — receives server-pushed messages and handles them locally.
3
- *
4
- * Connects to fathom-server's /ws/agent/{workspace} endpoint. Receives:
5
- * - ping → respond with pong
6
- *
7
- * Stream-json agents handle inject/ping_fire via subprocess stdin — the server
8
- * writes directly, so no tmux injection is needed here.
9
- *
10
- * Auto-reconnects with exponential backoff (1s → 60s cap).
11
- */
12
-
13
- import WebSocket from "ws";
14
-
15
- const KEEPALIVE_INTERVAL_MS = 30_000;
16
- const INITIAL_RECONNECT_MS = 1_000;
17
- const MAX_RECONNECT_MS = 60_000;
18
-
19
- /**
20
- * @param {object} config — resolved config from config.js
21
- * @returns {{ close: () => void }}
22
- */
23
- export function createWSConnection(config) {
24
- const workspace = config.workspace;
25
- const agent = config.agents?.[0] || "unknown";
26
- const vaultMode = config.vaultMode || "local";
27
-
28
- // Derive WS URL from HTTP server URL
29
- const serverUrl = config.server || "http://localhost:4243";
30
- const wsUrl = serverUrl
31
- .replace(/^http:/, "ws:")
32
- .replace(/^https:/, "wss:")
33
- + `/ws/agent/${encodeURIComponent(workspace)}`;
34
-
35
- let ws = null;
36
- let reconnectDelay = INITIAL_RECONNECT_MS;
37
- let keepaliveTimer = null;
38
- let closed = false;
39
-
40
- connect();
41
-
42
- function connect() {
43
- if (closed) return;
44
-
45
- console.error(`[ws] connecting to ${wsUrl}`);
46
-
47
- try {
48
- ws = new WebSocket(wsUrl);
49
- } catch (err) {
50
- console.error(`[ws] connection constructor failed: ${err.message}`);
51
- scheduleReconnect();
52
- return;
53
- }
54
-
55
- ws.on("open", () => {
56
- console.error(`[ws] connected — sending hello (agent=${agent}, vault_mode=${vaultMode})`);
57
- reconnectDelay = INITIAL_RECONNECT_MS;
58
-
59
- // Send hello handshake (token sent here, not in URL)
60
- ws.send(JSON.stringify({
61
- type: "hello",
62
- agent,
63
- vault_mode: vaultMode,
64
- token: config.apiKey || "",
65
- }));
66
-
67
- // Start keepalive pong timer
68
- startKeepalive();
69
- });
70
-
71
- ws.on("message", (raw) => {
72
- let msg;
73
- try {
74
- msg = JSON.parse(raw.toString());
75
- } catch {
76
- return;
77
- }
78
-
79
- switch (msg.type) {
80
- case "welcome":
81
- console.error(`[ws] welcome received — connection established for workspace=${workspace}`);
82
- break;
83
-
84
- case "inject":
85
- case "ping_fire":
86
- // Stream-json agents handle injection via subprocess stdin on the server side
87
- console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars) — handled by server subprocess`);
88
- break;
89
-
90
- case "ping":
91
- safeSend({ type: "pong" });
92
- break;
93
-
94
- case "error":
95
- console.error(`[ws] server error: ${msg.message || JSON.stringify(msg)}`);
96
- // Server rejected us — don't reconnect immediately
97
- reconnectDelay = MAX_RECONNECT_MS;
98
- break;
99
- }
100
- });
101
-
102
- ws.on("close", (code, reason) => {
103
- console.error(`[ws] closed (code=${code}, reason=${reason || "none"})`);
104
- stopKeepalive();
105
- if (!closed) scheduleReconnect();
106
- });
107
-
108
- ws.on("error", (err) => {
109
- console.error(`[ws] error: ${err.message}`);
110
- // Error always followed by close event — reconnect handled there
111
- stopKeepalive();
112
- });
113
- }
114
-
115
- function safeSend(obj) {
116
- try {
117
- if (ws && ws.readyState === WebSocket.OPEN) {
118
- ws.send(JSON.stringify(obj));
119
- }
120
- } catch {
121
- // Swallow — close event will trigger reconnect
122
- }
123
- }
124
-
125
- function startKeepalive() {
126
- stopKeepalive();
127
- keepaliveTimer = setInterval(() => {
128
- safeSend({ type: "pong" });
129
- }, KEEPALIVE_INTERVAL_MS);
130
- }
131
-
132
- function stopKeepalive() {
133
- if (keepaliveTimer) {
134
- clearInterval(keepaliveTimer);
135
- keepaliveTimer = null;
136
- }
137
- }
138
-
139
- function scheduleReconnect() {
140
- if (closed) return;
141
- console.error(`[ws] reconnecting in ${reconnectDelay}ms`);
142
- setTimeout(connect, reconnectDelay);
143
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
144
- }
145
-
146
- function close() {
147
- closed = true;
148
- stopKeepalive();
149
- if (ws) {
150
- try { ws.close(); } catch { /* ignore */ }
151
- ws = null;
152
- }
153
- }
154
-
155
- return { close };
156
- }