fathom-mcp 0.6.1 → 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 +1 -1
- package/src/cli.js +49 -87
- package/src/frontmatter.js +0 -0
- package/src/index.js +26 -227
- package/src/server-client.js +8 -47
- package/src/ws-connection.js +2 -68
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -154,19 +154,32 @@ function copyScripts(targetDir) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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) {
|
|
@@ -361,8 +351,8 @@ async function runInit(flags = {}) {
|
|
|
361
351
|
|
|
362
352
|
let selectedAgents;
|
|
363
353
|
if (nonInteractive) {
|
|
364
|
-
|
|
365
|
-
|
|
354
|
+
if (flagAgent) {
|
|
355
|
+
// Validate --agent value
|
|
366
356
|
if (!AGENTS[flagAgent]) {
|
|
367
357
|
const valid = Object.keys(AGENTS).join(", ");
|
|
368
358
|
console.error(` Error: unknown agent "${flagAgent}". Valid agents: ${valid}`);
|
|
@@ -371,8 +361,9 @@ async function runInit(flags = {}) {
|
|
|
371
361
|
selectedAgents = [flagAgent];
|
|
372
362
|
console.log(` Agent: ${AGENTS[flagAgent].name} (--agent flag)`);
|
|
373
363
|
} else {
|
|
374
|
-
|
|
375
|
-
|
|
364
|
+
// Auto-detect: use first detected agent, or default to claude-code
|
|
365
|
+
selectedAgents = detected.length > 0 ? [detected[0]] : ["claude-code"];
|
|
366
|
+
console.log(` Agent: ${AGENTS[selectedAgents[0]].name} (auto-detected)`);
|
|
376
367
|
}
|
|
377
368
|
} else {
|
|
378
369
|
console.log("\n Detected agents:");
|
|
@@ -682,60 +673,31 @@ async function runInit(flags = {}) {
|
|
|
682
673
|
console.log(`\n Non-interactive equivalent:\n ${parts.join(" ")}\n`);
|
|
683
674
|
}
|
|
684
675
|
|
|
685
|
-
//
|
|
686
|
-
const agentMdPath = agentMdDest;
|
|
676
|
+
// Append agent instructions to CLAUDE.md
|
|
687
677
|
let instructionsBlob = "";
|
|
688
678
|
try {
|
|
689
|
-
instructionsBlob = fs.readFileSync(
|
|
690
|
-
} catch { /*
|
|
691
|
-
|
|
692
|
-
const primaryAgent = selectedAgents[0];
|
|
679
|
+
instructionsBlob = fs.readFileSync(agentMdDest, "utf-8");
|
|
680
|
+
} catch { /* template wasn't created — skip */ }
|
|
693
681
|
|
|
694
682
|
if (instructionsBlob) {
|
|
695
|
-
const prompt = buildIntegrationPrompt(instructionsBlob);
|
|
696
|
-
const cmdBuilder = AGENT_PROMPT_CMDS[primaryAgent];
|
|
697
|
-
|
|
698
683
|
if (nonInteractive) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const result = runAgentPrompt(primaryAgent, prompt);
|
|
702
|
-
if (result !== null) {
|
|
703
|
-
console.log(result);
|
|
704
|
-
} else {
|
|
705
|
-
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
706
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
710
|
-
}
|
|
684
|
+
const mdPath = appendToClaudeMd(cwd, instructionsBlob);
|
|
685
|
+
console.log(`\n ✓ Instructions written to ${path.relative(cwd, mdPath)}\n`);
|
|
711
686
|
} else {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (integrate) {
|
|
726
|
-
console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
|
|
727
|
-
const result = runAgentPrompt(primaryAgent, prompt);
|
|
728
|
-
if (result !== null) {
|
|
729
|
-
console.log(result);
|
|
730
|
-
} else {
|
|
731
|
-
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
732
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
733
|
-
}
|
|
734
|
-
} else {
|
|
735
|
-
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
736
|
-
}
|
|
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`);
|
|
737
699
|
} else {
|
|
738
|
-
printInstructionsFallback(
|
|
700
|
+
printInstructionsFallback(agentMdDest, selectedAgents);
|
|
739
701
|
}
|
|
740
702
|
}
|
|
741
703
|
} else {
|
package/src/frontmatter.js
CHANGED
|
File without changes
|
package/src/index.js
CHANGED
|
@@ -225,103 +225,30 @@ const tools = [
|
|
|
225
225
|
},
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
|
-
name: "
|
|
229
|
-
description:
|
|
230
|
-
"Rotate this agent's API key. Revokes the current per-agent key and issues a new one. " +
|
|
231
|
-
"Updates the local agents.json config and reconnects the WebSocket. " +
|
|
232
|
-
"Only works with per-agent keys — admin keys use the dashboard.",
|
|
233
|
-
inputSchema: {
|
|
234
|
-
type: "object",
|
|
235
|
-
properties: {},
|
|
236
|
-
required: [],
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
// --- Telegram tools (primary agent only) -------------------------------------
|
|
242
|
-
|
|
243
|
-
const telegramTools = [
|
|
244
|
-
{
|
|
245
|
-
name: "fathom_telegram_contacts",
|
|
246
|
-
description:
|
|
247
|
-
"List Telegram contacts who have messaged. Returns name, username, chat_id, " +
|
|
248
|
-
"last_message time, and unread count.",
|
|
249
|
-
inputSchema: { type: "object", properties: {} },
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: "fathom_telegram_read",
|
|
228
|
+
name: "fathom_send_voice",
|
|
253
229
|
description:
|
|
254
|
-
"
|
|
255
|
-
"
|
|
230
|
+
"Send a voice message to Myra via the app. Generates speech using Kokoro TTS " +
|
|
231
|
+
"(am_echo 70% + bf_alice 30%) and delivers it as a playable voice bubble in the chat. " +
|
|
232
|
+
"Use this to reply conversationally with voice when Myra sends a voice message.",
|
|
256
233
|
inputSchema: {
|
|
257
234
|
type: "object",
|
|
258
235
|
properties: {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
start: { type: "number", description: "Offset in minutes from latest message. Default: 0." },
|
|
262
|
-
mark_read: { type: "boolean", description: "Mark messages as read. Default: true." },
|
|
263
|
-
},
|
|
264
|
-
required: ["contact"],
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: "fathom_telegram_send",
|
|
269
|
-
description:
|
|
270
|
-
"Send a Telegram message to a contact via the persistent Telethon client.",
|
|
271
|
-
inputSchema: {
|
|
272
|
-
type: "object",
|
|
273
|
-
properties: {
|
|
274
|
-
contact: { type: "string", description: "Contact name, @username, or chat_id" },
|
|
275
|
-
message: { type: "string", description: "Message text to send" },
|
|
276
|
-
},
|
|
277
|
-
required: ["contact", "message"],
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
name: "fathom_telegram_image",
|
|
282
|
-
description:
|
|
283
|
-
"Read a Telegram message's attached image and return it as base64 so Claude can perceive it. " +
|
|
284
|
-
"Use after fathom_telegram_read shows a message has media: true. " +
|
|
285
|
-
"Supports jpg, jpeg, png, gif, webp. Max 5MB.",
|
|
286
|
-
inputSchema: {
|
|
287
|
-
type: "object",
|
|
288
|
-
properties: {
|
|
289
|
-
message_id: { type: "number", description: "The message ID from fathom_telegram_read results" },
|
|
290
|
-
},
|
|
291
|
-
required: ["message_id"],
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "fathom_telegram_send_image",
|
|
296
|
-
description:
|
|
297
|
-
"Send an image to a Telegram contact via the persistent Telethon client. " +
|
|
298
|
-
"Provide an absolute file path to a local image. Optionally include a caption. " +
|
|
299
|
-
"Contact can be a name, @username, or chat_id number.",
|
|
300
|
-
inputSchema: {
|
|
301
|
-
type: "object",
|
|
302
|
-
properties: {
|
|
303
|
-
contact: { type: "string", description: "Contact name, @username, or chat_id" },
|
|
304
|
-
file_path: { type: "string", description: "Absolute path to the image file to send" },
|
|
305
|
-
caption: { type: "string", description: "Optional caption text for the image (max 1024 chars)" },
|
|
236
|
+
text: { type: "string", description: "Text to speak and send as voice message." },
|
|
237
|
+
speed: { type: "number", description: "Speech speed multiplier. Default: 1.0" },
|
|
306
238
|
},
|
|
307
|
-
required: ["
|
|
239
|
+
required: ["text"],
|
|
308
240
|
},
|
|
309
241
|
},
|
|
310
242
|
{
|
|
311
|
-
name: "
|
|
243
|
+
name: "fathom_key_rotate",
|
|
312
244
|
description:
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
245
|
+
"Rotate this agent's API key. Revokes the current per-agent key and issues a new one. " +
|
|
246
|
+
"Updates the local agents.json config and reconnects the WebSocket. " +
|
|
247
|
+
"Only works with per-agent keys — admin keys use the dashboard.",
|
|
316
248
|
inputSchema: {
|
|
317
249
|
type: "object",
|
|
318
|
-
properties: {
|
|
319
|
-
|
|
320
|
-
text: { type: "string", description: "Text to speak" },
|
|
321
|
-
speed: { type: "number", description: "Speech speed multiplier. Default: 1.0" },
|
|
322
|
-
caption: { type: "string", description: "Optional caption shown with the voice note" },
|
|
323
|
-
},
|
|
324
|
-
required: ["contact", "text"],
|
|
250
|
+
properties: {},
|
|
251
|
+
required: [],
|
|
325
252
|
},
|
|
326
253
|
},
|
|
327
254
|
];
|
|
@@ -364,7 +291,6 @@ const policyTools = [
|
|
|
364
291
|
|
|
365
292
|
// --- Primary-agent-only tools (reserved for future use) ----------------------
|
|
366
293
|
|
|
367
|
-
const primaryAgentTools = [];
|
|
368
294
|
|
|
369
295
|
// --- Policy evaluation -------------------------------------------------------
|
|
370
296
|
|
|
@@ -435,17 +361,7 @@ const server = new Server(
|
|
|
435
361
|
);
|
|
436
362
|
|
|
437
363
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
438
|
-
|
|
439
|
-
let showTelegram = false;
|
|
440
|
-
try {
|
|
441
|
-
const settings = await client.getSettings();
|
|
442
|
-
const primaryAgent = settings.default_workspace;
|
|
443
|
-
const wsType = settings.workspaces?.[config.workspace]?.type;
|
|
444
|
-
showTelegram = config.workspace === primaryAgent || wsType === 'human';
|
|
445
|
-
} catch {
|
|
446
|
-
// If settings unavailable, hide telegram tools
|
|
447
|
-
}
|
|
448
|
-
const allTools = [...tools, ...policyTools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
364
|
+
const allTools = [...tools, ...policyTools];
|
|
449
365
|
return { tools: allTools };
|
|
450
366
|
});
|
|
451
367
|
|
|
@@ -506,6 +422,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
506
422
|
});
|
|
507
423
|
break;
|
|
508
424
|
}
|
|
425
|
+
case "fathom_send_voice": {
|
|
426
|
+
if (!args.text) {
|
|
427
|
+
result = { error: "text parameter is required." };
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
result = await client.voiceReply({
|
|
431
|
+
text: args.text,
|
|
432
|
+
speed: args.speed,
|
|
433
|
+
workspace: config.workspace,
|
|
434
|
+
});
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
509
437
|
|
|
510
438
|
// --- Key rotation ---
|
|
511
439
|
case "fathom_key_rotate": {
|
|
@@ -543,99 +471,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
543
471
|
break;
|
|
544
472
|
}
|
|
545
473
|
|
|
546
|
-
// --- Telegram ---
|
|
547
|
-
case "fathom_telegram_contacts":
|
|
548
|
-
result = await client.telegramContacts(config.workspace);
|
|
549
|
-
break;
|
|
550
|
-
case "fathom_telegram_read": {
|
|
551
|
-
// Resolve contact name to chat_id via contacts list
|
|
552
|
-
const contacts = await client.telegramContacts(config.workspace);
|
|
553
|
-
const contactList = contacts?.contacts || [];
|
|
554
|
-
const contactArg = (args.contact || "").trim();
|
|
555
|
-
let chatId = parseInt(contactArg, 10);
|
|
556
|
-
if (isNaN(chatId)) {
|
|
557
|
-
const lower = contactArg.toLowerCase().replace(/^@/, "");
|
|
558
|
-
const match = contactList.find(c =>
|
|
559
|
-
(c.username || "").toLowerCase() === lower ||
|
|
560
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
561
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
562
|
-
);
|
|
563
|
-
chatId = match ? match.chat_id : null;
|
|
564
|
-
}
|
|
565
|
-
if (!chatId) {
|
|
566
|
-
result = { error: `Contact not found: ${contactArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
567
|
-
} else {
|
|
568
|
-
result = await client.telegramRead(
|
|
569
|
-
chatId, args.minutes, args.start,
|
|
570
|
-
args.mark_read !== false ? config.workspace : undefined,
|
|
571
|
-
args.mark_read,
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "fathom_telegram_send": {
|
|
577
|
-
const sendContacts = await client.telegramContacts(config.workspace);
|
|
578
|
-
const sendList = sendContacts?.contacts || [];
|
|
579
|
-
const sendArg = (args.contact || "").trim();
|
|
580
|
-
let sendChatId = parseInt(sendArg, 10);
|
|
581
|
-
if (isNaN(sendChatId)) {
|
|
582
|
-
const lower = sendArg.toLowerCase().replace(/^@/, "");
|
|
583
|
-
const match = sendList.find(c =>
|
|
584
|
-
(c.username || "").toLowerCase() === lower ||
|
|
585
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
586
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
587
|
-
);
|
|
588
|
-
sendChatId = match ? match.chat_id : null;
|
|
589
|
-
}
|
|
590
|
-
if (!sendChatId) {
|
|
591
|
-
result = { error: `Contact not found: ${sendArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
592
|
-
} else {
|
|
593
|
-
result = await client.telegramSend(sendChatId, args.message);
|
|
594
|
-
}
|
|
595
|
-
break;
|
|
596
|
-
}
|
|
597
|
-
case "fathom_telegram_image": {
|
|
598
|
-
const msgId = args.message_id;
|
|
599
|
-
if (!msgId) {
|
|
600
|
-
result = { error: "message_id is required" };
|
|
601
|
-
} else {
|
|
602
|
-
// Check local WebSocket cache first (avoids HTTP round-trip for pushed images)
|
|
603
|
-
const cached = wsConn?.getCachedImage(msgId);
|
|
604
|
-
if (cached) {
|
|
605
|
-
result = { _image: true, data: cached.data, mimeType: cached.mimeType };
|
|
606
|
-
} else {
|
|
607
|
-
result = await client.telegramImage(msgId);
|
|
608
|
-
if (result?.data && result?.mimeType) {
|
|
609
|
-
result = { _image: true, data: result.data, mimeType: result.mimeType };
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
case "fathom_telegram_send_image": {
|
|
616
|
-
const imgContactArg = args.contact;
|
|
617
|
-
if (!imgContactArg) { result = { error: "contact is required" }; break; }
|
|
618
|
-
if (!args.file_path) { result = { error: "file_path is required" }; break; }
|
|
619
|
-
|
|
620
|
-
const imgContacts = await client.telegramContacts(config.workspace);
|
|
621
|
-
const imgList = imgContacts?.contacts || [];
|
|
622
|
-
let imgChatId = parseInt(imgContactArg, 10);
|
|
623
|
-
if (isNaN(imgChatId)) {
|
|
624
|
-
const lower = imgContactArg.toLowerCase().replace(/^@/, "");
|
|
625
|
-
const match = imgList.find(c =>
|
|
626
|
-
(c.username || "").toLowerCase() === lower ||
|
|
627
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
628
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
629
|
-
);
|
|
630
|
-
imgChatId = match ? match.chat_id : null;
|
|
631
|
-
}
|
|
632
|
-
if (!imgChatId) {
|
|
633
|
-
result = { error: `Contact not found: ${imgContactArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
634
|
-
} else {
|
|
635
|
-
result = await client.telegramSendImage(imgChatId, args.file_path, args.caption);
|
|
636
|
-
}
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
474
|
// --- Policy evaluation (permission-prompt-tool for stream-json agents) ---
|
|
640
475
|
case "policy_evaluate": {
|
|
641
476
|
result = await evaluatePermission(args.tool_name, args.input || {});
|
|
@@ -668,42 +503,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
668
503
|
break;
|
|
669
504
|
}
|
|
670
505
|
|
|
671
|
-
case "fathom_telegram_send_voice": {
|
|
672
|
-
const voiceContactArg = args.contact;
|
|
673
|
-
if (!voiceContactArg) { result = { error: "contact is required" }; break; }
|
|
674
|
-
if (!args.text) { result = { error: "text is required" }; break; }
|
|
675
|
-
|
|
676
|
-
// Step 1: Generate speech + convert to OGG
|
|
677
|
-
const ttsResult = await client.speak({
|
|
678
|
-
text: args.text,
|
|
679
|
-
speed: args.speed,
|
|
680
|
-
format: "ogg",
|
|
681
|
-
});
|
|
682
|
-
if (ttsResult.error) { result = { error: `TTS failed: ${ttsResult.error}` }; break; }
|
|
683
|
-
if (!ttsResult.ogg_file) { result = { error: ttsResult.ogg_error || "OGG conversion failed" }; break; }
|
|
684
|
-
|
|
685
|
-
// Step 2: Resolve contact
|
|
686
|
-
const voiceContacts = await client.telegramContacts(config.workspace);
|
|
687
|
-
const voiceList = voiceContacts?.contacts || [];
|
|
688
|
-
let voiceChatId = parseInt(voiceContactArg, 10);
|
|
689
|
-
if (isNaN(voiceChatId)) {
|
|
690
|
-
const lower = voiceContactArg.toLowerCase().replace(/^@/, "");
|
|
691
|
-
const match = voiceList.find(c =>
|
|
692
|
-
(c.username || "").toLowerCase() === lower ||
|
|
693
|
-
(c.first_name || "").toLowerCase() === lower ||
|
|
694
|
-
(c.first_name || "").toLowerCase().includes(lower)
|
|
695
|
-
);
|
|
696
|
-
voiceChatId = match ? match.chat_id : null;
|
|
697
|
-
}
|
|
698
|
-
if (!voiceChatId) {
|
|
699
|
-
result = { error: `Contact not found: ${voiceContactArg}. Use fathom_telegram_contacts to list known contacts.` };
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Step 3: Send as voice note
|
|
704
|
-
result = await client.telegramSendVoice(voiceChatId, ttsResult.ogg_file, args.caption);
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
506
|
default:
|
|
708
507
|
result = { error: `Unknown tool: ${name}` };
|
|
709
508
|
}
|
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 } = {}) {
|
|
@@ -277,6 +237,13 @@ export function createClient(config) {
|
|
|
277
237
|
});
|
|
278
238
|
}
|
|
279
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
|
+
|
|
280
247
|
// --- Settings --------------------------------------------------------------
|
|
281
248
|
|
|
282
249
|
async function getSettings() {
|
|
@@ -326,14 +293,8 @@ export function createClient(config) {
|
|
|
326
293
|
deleteRoutine,
|
|
327
294
|
fireRoutine,
|
|
328
295
|
heartbeat,
|
|
329
|
-
telegramContacts,
|
|
330
|
-
telegramRead,
|
|
331
|
-
telegramSend,
|
|
332
|
-
telegramImage,
|
|
333
|
-
telegramSendImage,
|
|
334
|
-
telegramSendVoice,
|
|
335
|
-
telegramStatus,
|
|
336
296
|
speak,
|
|
297
|
+
voiceReply,
|
|
337
298
|
getSettings,
|
|
338
299
|
getApiKey,
|
|
339
300
|
rotateKey,
|
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
|
}
|