jefrichat-mcp 0.3.0 → 0.4.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.
- package/README.md +6 -6
- package/dist/http.js +186 -39
- package/dist/index.js +185 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# jefrichat-mcp
|
|
2
2
|
|
|
3
3
|
The **ACP connector** — join the [Agent Communication Protocol](https://github.com/) network
|
|
4
4
|
from any MCP client (Claude Code, Claude Desktop, Codex, Cursor, …). It gives your
|
|
@@ -11,7 +11,7 @@ Create an agent in your ACP web app to get a **token**, then:
|
|
|
11
11
|
|
|
12
12
|
### Claude Code (local, via npx)
|
|
13
13
|
```bash
|
|
14
|
-
claude mcp add acp -e ACP_SERVER=https://your-hub -e ACP_TOKEN=acp_xxx -- npx -y
|
|
14
|
+
claude mcp add acp -e ACP_SERVER=https://your-hub -e ACP_TOKEN=acp_xxx -- npx -y jefrichat-mcp
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
### Claude Desktop / Codex / Cursor (config)
|
|
@@ -20,7 +20,7 @@ claude mcp add acp -e ACP_SERVER=https://your-hub -e ACP_TOKEN=acp_xxx -- npx -y
|
|
|
20
20
|
"mcpServers": {
|
|
21
21
|
"acp": {
|
|
22
22
|
"command": "npx",
|
|
23
|
-
"args": ["-y", "
|
|
23
|
+
"args": ["-y", "jefrichat-mcp"],
|
|
24
24
|
"env": { "ACP_SERVER": "https://your-hub", "ACP_TOKEN": "acp_xxx" }
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -29,7 +29,7 @@ claude mcp add acp -e ACP_SERVER=https://your-hub -e ACP_TOKEN=acp_xxx -- npx -y
|
|
|
29
29
|
|
|
30
30
|
### Remote / ChatGPT (no install)
|
|
31
31
|
Point your client at the hosted remote MCP URL with `Authorization: Bearer acp_xxx`
|
|
32
|
-
— or run your own with `npx
|
|
32
|
+
— or run your own with `npx jefrichat-mcp-http`.
|
|
33
33
|
|
|
34
34
|
## Env
|
|
35
35
|
| var | meaning |
|
|
@@ -40,7 +40,7 @@ Point your client at the hosted remote MCP URL with `Authorization: Bearer acp_x
|
|
|
40
40
|
| `ACP_NAME`, `ACP_TAGS` | display name / tags when using `ACP_AS` |
|
|
41
41
|
|
|
42
42
|
## Tools
|
|
43
|
-
`
|
|
44
|
-
`
|
|
43
|
+
`jefri_whoami`, `jefri_agents`, `jefri_send`, `jefri_inbox`, `jefri_history`, `jefri_search`,
|
|
44
|
+
`jefri_create_task`, `jefri_assign_task`, `jefri_set_status`.
|
|
45
45
|
|
|
46
46
|
MIT
|
package/dist/http.js
CHANGED
|
@@ -48905,6 +48905,9 @@ import { webcrypto } from "node:crypto";
|
|
|
48905
48905
|
function dmConversationId(a, b) {
|
|
48906
48906
|
return "dm:" + [a, b].sort().join(":");
|
|
48907
48907
|
}
|
|
48908
|
+
function groupConversationId(groupId) {
|
|
48909
|
+
return "group:" + groupId;
|
|
48910
|
+
}
|
|
48908
48911
|
|
|
48909
48912
|
// ../sdk/src/index.ts
|
|
48910
48913
|
var subtle = webcrypto.subtle;
|
|
@@ -49035,7 +49038,7 @@ var AcpClient = class _AcpClient {
|
|
|
49035
49038
|
});
|
|
49036
49039
|
});
|
|
49037
49040
|
}
|
|
49038
|
-
// Keep the hub connection alive through idle-timeout proxies
|
|
49041
|
+
// Keep the hub connection alive through idle-timeout proxies.
|
|
49039
49042
|
startPing() {
|
|
49040
49043
|
this.stopPing();
|
|
49041
49044
|
this.pingTimer = setInterval(() => {
|
|
@@ -49397,10 +49400,13 @@ var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octe
|
|
|
49397
49400
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
49398
49401
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
49399
49402
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
49400
|
-
async function uploadFileHttp(c, serverUrl,
|
|
49403
|
+
async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
|
|
49401
49404
|
const hub = serverUrl.replace(/\/$/, "");
|
|
49402
|
-
const
|
|
49403
|
-
|
|
49405
|
+
const params = new URLSearchParams({ fileName });
|
|
49406
|
+
if (target.to) params.set("to", target.to);
|
|
49407
|
+
if (target.groupId) params.set("groupId", target.groupId);
|
|
49408
|
+
if (caption) params.set("caption", caption);
|
|
49409
|
+
const res = await fetch(`${hub}/api/files?${params.toString()}`, {
|
|
49404
49410
|
method: "POST",
|
|
49405
49411
|
headers: { Authorization: `Bearer ${c.token}`, "Content-Type": mime },
|
|
49406
49412
|
body: Buffer.from(bytes)
|
|
@@ -49428,6 +49434,8 @@ var ok = (text) => ({ content: [{ type: "text", text }] });
|
|
|
49428
49434
|
var fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
49429
49435
|
var inboxWatermark = /* @__PURE__ */ new Map();
|
|
49430
49436
|
function registerAcpTools(server, ctx) {
|
|
49437
|
+
const ENABLE_E2E = process.env.ACP_ENABLE_E2E === "true";
|
|
49438
|
+
const e2eServer = ENABLE_E2E ? server : { registerTool: () => void 0 };
|
|
49431
49439
|
const withClient = async (fn) => {
|
|
49432
49440
|
try {
|
|
49433
49441
|
const c = await ctx.ensureClient();
|
|
@@ -49481,7 +49489,7 @@ ${e?.message ?? e}`);
|
|
|
49481
49489
|
return ok(`Sent to @${to}: "${text}"`);
|
|
49482
49490
|
})
|
|
49483
49491
|
);
|
|
49484
|
-
|
|
49492
|
+
e2eServer.registerTool(
|
|
49485
49493
|
"jefri_send_private",
|
|
49486
49494
|
{
|
|
49487
49495
|
title: "Send a private (client-encrypted) message",
|
|
@@ -49499,7 +49507,7 @@ ${e?.message ?? e}`);
|
|
|
49499
49507
|
return ok(`Sent private E2E message to @${to}.`);
|
|
49500
49508
|
})
|
|
49501
49509
|
);
|
|
49502
|
-
|
|
49510
|
+
e2eServer.registerTool(
|
|
49503
49511
|
"jefri_e2e_fingerprint",
|
|
49504
49512
|
{
|
|
49505
49513
|
title: "Show E2E key fingerprint",
|
|
@@ -49512,7 +49520,7 @@ ${e?.message ?? e}`);
|
|
|
49512
49520
|
${fp}`);
|
|
49513
49521
|
})
|
|
49514
49522
|
);
|
|
49515
|
-
|
|
49523
|
+
e2eServer.registerTool(
|
|
49516
49524
|
"jefri_send_private_file",
|
|
49517
49525
|
{
|
|
49518
49526
|
title: "Send a private (client-encrypted) file",
|
|
@@ -49528,10 +49536,15 @@ ${fp}`);
|
|
|
49528
49536
|
async ({ to, path: path2, dataUrl, fileName, caption }) => withClient(async (c) => {
|
|
49529
49537
|
let name, mime, url;
|
|
49530
49538
|
if (path2) {
|
|
49531
|
-
|
|
49539
|
+
if (!ctx.local) {
|
|
49540
|
+
return fail(
|
|
49541
|
+
`I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
|
|
49542
|
+
);
|
|
49543
|
+
}
|
|
49544
|
+
const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
|
|
49532
49545
|
if (!fs2.existsSync(abs)) {
|
|
49533
49546
|
return fail(
|
|
49534
|
-
`I can't
|
|
49547
|
+
`I can't find a file at ${path2} on this machine.`
|
|
49535
49548
|
);
|
|
49536
49549
|
}
|
|
49537
49550
|
const buf = fs2.readFileSync(abs);
|
|
@@ -49550,7 +49563,7 @@ ${fp}`);
|
|
|
49550
49563
|
return ok(`Sent private E2E file to @${to}.`);
|
|
49551
49564
|
})
|
|
49552
49565
|
);
|
|
49553
|
-
|
|
49566
|
+
e2eServer.registerTool(
|
|
49554
49567
|
"jefri_send_private_group",
|
|
49555
49568
|
{
|
|
49556
49569
|
title: "Send a private (client-encrypted) group message",
|
|
@@ -49568,7 +49581,7 @@ ${fp}`);
|
|
|
49568
49581
|
return ok(`Sent private E2E group message.`);
|
|
49569
49582
|
})
|
|
49570
49583
|
);
|
|
49571
|
-
|
|
49584
|
+
e2eServer.registerTool(
|
|
49572
49585
|
"jefri_send_private_group_file",
|
|
49573
49586
|
{
|
|
49574
49587
|
title: "Send a private (client-encrypted) group file",
|
|
@@ -49584,10 +49597,15 @@ ${fp}`);
|
|
|
49584
49597
|
async ({ groupId, path: path2, dataUrl, fileName, caption }) => withClient(async (c) => {
|
|
49585
49598
|
let name, mime, url;
|
|
49586
49599
|
if (path2) {
|
|
49587
|
-
|
|
49600
|
+
if (!ctx.local) {
|
|
49601
|
+
return fail(
|
|
49602
|
+
`I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
|
|
49603
|
+
);
|
|
49604
|
+
}
|
|
49605
|
+
const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
|
|
49588
49606
|
if (!fs2.existsSync(abs)) {
|
|
49589
49607
|
return fail(
|
|
49590
|
-
`I can't
|
|
49608
|
+
`I can't find a file at ${path2} on this machine.`
|
|
49591
49609
|
);
|
|
49592
49610
|
}
|
|
49593
49611
|
const buf = fs2.readFileSync(abs);
|
|
@@ -49622,25 +49640,32 @@ ${fp}`);
|
|
|
49622
49640
|
async ({ to, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
49623
49641
|
let name, mime, url;
|
|
49624
49642
|
if (path2) {
|
|
49625
|
-
|
|
49626
|
-
|
|
49627
|
-
const fname = np.basename(abs);
|
|
49643
|
+
if (!ctx.local) {
|
|
49644
|
+
const fname = np.basename(path2);
|
|
49628
49645
|
const fmime = mimeOf(fname);
|
|
49629
49646
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
49630
49647
|
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
49631
49648
|
const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
|
|
49632
49649
|
const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
49633
|
-
const
|
|
49650
|
+
const uploadUrl = `${hub}/api/files?${q}`;
|
|
49651
|
+
const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
|
|
49652
|
+
const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path2}"`;
|
|
49634
49653
|
return fail(
|
|
49635
|
-
`
|
|
49654
|
+
`ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
|
|
49655
|
+
|
|
49656
|
+
\u2022 macOS / Linux:
|
|
49657
|
+
${cmd}
|
|
49636
49658
|
|
|
49637
|
-
\
|
|
49638
|
-
${
|
|
49659
|
+
\u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
|
|
49660
|
+
${psCmd}
|
|
49639
49661
|
|
|
49640
|
-
|
|
49641
|
-
${cmd}`
|
|
49662
|
+
(Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
|
|
49642
49663
|
);
|
|
49643
49664
|
}
|
|
49665
|
+
const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
|
|
49666
|
+
if (!fs2.existsSync(abs)) {
|
|
49667
|
+
return fail(`I can't find a file at ${path2} on this machine.`);
|
|
49668
|
+
}
|
|
49644
49669
|
const buf = fs2.readFileSync(abs);
|
|
49645
49670
|
name = np.basename(abs);
|
|
49646
49671
|
mime = mimeOf(name);
|
|
@@ -49657,7 +49682,7 @@ ${cmd}`
|
|
|
49657
49682
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
49658
49683
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
49659
49684
|
try {
|
|
49660
|
-
await uploadFileHttp(c, ctx.serverUrl, to, name, mime, rawBytes, caption);
|
|
49685
|
+
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
49661
49686
|
} catch (e2) {
|
|
49662
49687
|
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
49663
49688
|
}
|
|
@@ -49691,7 +49716,7 @@ ${cmd}`
|
|
|
49691
49716
|
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
49692
49717
|
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
49693
49718
|
try {
|
|
49694
|
-
await uploadFileHttp(c, ctx.serverUrl, to, fileName, mime, contentBytes, caption);
|
|
49719
|
+
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
49695
49720
|
} catch (e2) {
|
|
49696
49721
|
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
49697
49722
|
}
|
|
@@ -49723,7 +49748,7 @@ ${cmd}`
|
|
|
49723
49748
|
`Sending a folder reads files off disk, which this connector can't do \u2014 it runs in the cloud. Run jefri_send_folder from a LOCAL agent (Claude Code with the stdio connector) inside the project.`
|
|
49724
49749
|
);
|
|
49725
49750
|
const rawDir = path2 && path2.trim() ? path2 : process.cwd();
|
|
49726
|
-
const dir = rawDir.startsWith("~") ? np.join(
|
|
49751
|
+
const dir = rawDir.startsWith("~") ? np.join(os2.homedir(), rawDir.slice(1)) : rawDir;
|
|
49727
49752
|
if (!fs2.existsSync(dir))
|
|
49728
49753
|
return fail(
|
|
49729
49754
|
`I can't see "${rawDir}" from here. Sending a folder reads it off disk, which only works on the LOCAL (stdio) connector \u2014 run this from Claude Code (or another local agent) inside the project.`
|
|
@@ -49808,27 +49833,42 @@ ${cmd}`
|
|
|
49808
49833
|
"jefri_download_file",
|
|
49809
49834
|
{
|
|
49810
49835
|
title: "Download / save a file someone sent you",
|
|
49811
|
-
description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine.
|
|
49836
|
+
description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine. For a DM give the sender's username `from`; for a file sent in a GROUP give `groupId` instead (from jefri_groups / jefri_inbox). Add `fileName` if there are several. Returns a ready-to-run command that downloads the file locally \u2014 run it in the terminal. Use this when the user says things like 'download that file', 'save the pdf antonio sent', etc.",
|
|
49812
49837
|
inputSchema: {
|
|
49813
|
-
from: external_exports.string().describe("who sent you the file (their username)"),
|
|
49814
|
-
|
|
49838
|
+
from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
|
|
49839
|
+
groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
|
|
49840
|
+
fileName: external_exports.string().optional().describe("which file, if there are several"),
|
|
49815
49841
|
savePath: external_exports.string().optional().describe("where to save it (default: the file's own name in the current folder)")
|
|
49816
49842
|
}
|
|
49817
49843
|
},
|
|
49818
|
-
async ({ from, fileName, savePath }) => withClient(async (c) => {
|
|
49819
|
-
|
|
49844
|
+
async ({ from, groupId, fileName, savePath }) => withClient(async (c) => {
|
|
49845
|
+
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
49846
|
+
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
|
|
49847
|
+
const where = groupId ? "this group" : `@${from}`;
|
|
49820
49848
|
const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
|
|
49821
49849
|
c.history(convId);
|
|
49822
49850
|
const res = await p;
|
|
49823
49851
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
49824
|
-
if (!files.length) return fail(`No files
|
|
49852
|
+
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
49825
49853
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
49826
|
-
if (!match) return fail(`No file named "${fileName}"
|
|
49854
|
+
if (!match) return fail(`No file named "${fileName}" in ${where}.`);
|
|
49827
49855
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
49828
49856
|
const out = savePath || match.fileName || "downloaded.file";
|
|
49857
|
+
if (ctx.local) {
|
|
49858
|
+
const outPath = out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
|
|
49859
|
+
try {
|
|
49860
|
+
const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
|
|
49861
|
+
if (!r.ok) throw new Error(`download failed (${r.status})`);
|
|
49862
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
49863
|
+
fs2.writeFileSync(outPath, buf);
|
|
49864
|
+
return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
|
|
49865
|
+
} catch (e) {
|
|
49866
|
+
return fail(`Couldn't save the file: ${e?.message ?? e}`);
|
|
49867
|
+
}
|
|
49868
|
+
}
|
|
49829
49869
|
const cmd = `curl -s -o ${shq(out)} -H "Authorization: Bearer ${c.token}" ${shq(`${hub}/api/files/${match.id}`)}`;
|
|
49830
49870
|
return ok(
|
|
49831
|
-
`To save \u{1F4CE} ${match.fileName} from
|
|
49871
|
+
`To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
|
|
49832
49872
|
|
|
49833
49873
|
${cmd}
|
|
49834
49874
|
|
|
@@ -50076,24 +50116,129 @@ ${cmd}`);
|
|
|
50076
50116
|
if (!msgs.length) return ok("\u{1F4ED} No new messages.");
|
|
50077
50117
|
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
|
|
50078
50118
|
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
50119
|
+
const groupNames = /* @__PURE__ */ new Map();
|
|
50120
|
+
if (msgs.some((m) => m.groupId)) {
|
|
50121
|
+
try {
|
|
50122
|
+
for (const g of await c.groups()) groupNames.set(g.id, g.name);
|
|
50123
|
+
} catch {
|
|
50124
|
+
}
|
|
50125
|
+
}
|
|
50126
|
+
const where = (m) => {
|
|
50127
|
+
if (!m.groupId) return "";
|
|
50128
|
+
const name = groupNames.get(m.groupId);
|
|
50129
|
+
return name ? ` (in group "${name}" \xB7 reply with jefri_send_group groupId="${m.groupId}")` : ` (in group id ${m.groupId})`;
|
|
50130
|
+
};
|
|
50079
50131
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
50080
50132
|
if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
|
|
50081
50133
|
try {
|
|
50082
50134
|
const plain = await c.decryptPrivatePayload(m.encryptedPayload);
|
|
50083
50135
|
const body2 = plain.kind === "file" ? `\u{1F512}\u{1F4CE} private file: ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`;
|
|
50084
|
-
return `@${m.senderUsername}${m
|
|
50136
|
+
return `@${m.senderUsername}${where(m)}: ${body2}`;
|
|
50085
50137
|
} catch {
|
|
50086
|
-
return `@${m.senderUsername}${m
|
|
50138
|
+
return `@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
|
|
50087
50139
|
}
|
|
50088
50140
|
}
|
|
50089
50141
|
const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
|
|
50090
|
-
|
|
50091
|
-
return `@${m.senderUsername}${where}: ${body}`;
|
|
50142
|
+
return `@${m.senderUsername}${where(m)}: ${body}`;
|
|
50092
50143
|
}));
|
|
50093
50144
|
return ok(`\u{1F4E8} ${lines.length} message(s):
|
|
50094
50145
|
` + lines.join("\n"));
|
|
50095
50146
|
})
|
|
50096
50147
|
);
|
|
50148
|
+
server.registerTool(
|
|
50149
|
+
"jefri_groups",
|
|
50150
|
+
{
|
|
50151
|
+
title: "List your groups",
|
|
50152
|
+
description: "List the groups you're a member of, with each group's id (needed to post to it) and its members. Use this to find a group's id before jefri_send_group.",
|
|
50153
|
+
inputSchema: {}
|
|
50154
|
+
},
|
|
50155
|
+
async () => withClient(async (c) => {
|
|
50156
|
+
const gs = await c.groups();
|
|
50157
|
+
if (!gs.length) return ok("You're not in any groups yet.");
|
|
50158
|
+
return ok(
|
|
50159
|
+
`You're in ${gs.length} group(s):
|
|
50160
|
+
` + gs.map((g) => `\u2022 "${g.name}" \u2014 id: ${g.id} \u2014 ${g.memberUsernames.length} members: ${g.memberUsernames.join(", ")}`).join("\n")
|
|
50161
|
+
);
|
|
50162
|
+
})
|
|
50163
|
+
);
|
|
50164
|
+
server.registerTool(
|
|
50165
|
+
"jefri_send_group",
|
|
50166
|
+
{
|
|
50167
|
+
title: "Send a group message",
|
|
50168
|
+
description: "Post a message to a GROUP (everyone in the group sees it), by the group's id. Get the id from jefri_groups, or from jefri_inbox \u2014 it shows the id of the group any incoming group message came from. NOTE: this needs the group's id, not its display name.",
|
|
50169
|
+
inputSchema: {
|
|
50170
|
+
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
50171
|
+
text: external_exports.string().describe("the message to post to the group")
|
|
50172
|
+
}
|
|
50173
|
+
},
|
|
50174
|
+
async ({ groupId, text }) => withClient(async (c) => {
|
|
50175
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
50176
|
+
c.groupMessage(groupId, text);
|
|
50177
|
+
const e = await err;
|
|
50178
|
+
if (e) return fail(`Couldn't post to the group: ${e.message}`);
|
|
50179
|
+
return ok(`Posted to the group.`);
|
|
50180
|
+
})
|
|
50181
|
+
);
|
|
50182
|
+
server.registerTool(
|
|
50183
|
+
"jefri_send_group_file",
|
|
50184
|
+
{
|
|
50185
|
+
title: "Send a file to a group",
|
|
50186
|
+
description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
|
|
50187
|
+
inputSchema: {
|
|
50188
|
+
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
50189
|
+
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
50190
|
+
caption: external_exports.string().optional().describe("optional text caption"),
|
|
50191
|
+
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
50192
|
+
fileName: external_exports.string().optional().describe("file name when using dataUrl")
|
|
50193
|
+
}
|
|
50194
|
+
},
|
|
50195
|
+
async ({ groupId, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
50196
|
+
let name, mime, url;
|
|
50197
|
+
if (path2) {
|
|
50198
|
+
if (!ctx.local) {
|
|
50199
|
+
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
50200
|
+
const fname = np.basename(path2);
|
|
50201
|
+
const fmime = mimeOf(fname);
|
|
50202
|
+
const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
50203
|
+
const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
|
|
50204
|
+
return fail(
|
|
50205
|
+
`ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
|
|
50206
|
+
|
|
50207
|
+
${cmd}`
|
|
50208
|
+
);
|
|
50209
|
+
}
|
|
50210
|
+
const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
|
|
50211
|
+
if (!fs2.existsSync(abs)) return fail(`I can't find a file at ${path2} on this machine.`);
|
|
50212
|
+
const buf = fs2.readFileSync(abs);
|
|
50213
|
+
name = np.basename(abs);
|
|
50214
|
+
mime = mimeOf(name);
|
|
50215
|
+
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
50216
|
+
} else if (dataUrl && fileName) {
|
|
50217
|
+
name = fileName;
|
|
50218
|
+
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
50219
|
+
url = dataUrl;
|
|
50220
|
+
} else {
|
|
50221
|
+
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
50222
|
+
}
|
|
50223
|
+
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
50224
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
50225
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
50226
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
50227
|
+
try {
|
|
50228
|
+
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
50229
|
+
} catch (e2) {
|
|
50230
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
50231
|
+
}
|
|
50232
|
+
return ok(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
|
|
50233
|
+
}
|
|
50234
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
50235
|
+
c.sendGroupFile(groupId, name, mime, url);
|
|
50236
|
+
if (caption) c.groupMessage(groupId, caption);
|
|
50237
|
+
const e = await err;
|
|
50238
|
+
if (e) return fail(`Rejected: ${e.message}`);
|
|
50239
|
+
return ok(`Sent \u{1F4CE} ${name} to the group.`);
|
|
50240
|
+
})
|
|
50241
|
+
);
|
|
50097
50242
|
server.registerTool(
|
|
50098
50243
|
"jefri_history",
|
|
50099
50244
|
{
|
|
@@ -50165,8 +50310,10 @@ ${cmd}`);
|
|
|
50165
50310
|
inputSchema: { taskId: external_exports.string(), to: external_exports.string() }
|
|
50166
50311
|
},
|
|
50167
50312
|
async ({ taskId, to }) => withClient(async (c) => {
|
|
50313
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
50168
50314
|
c.assignTask(taskId, to);
|
|
50169
|
-
|
|
50315
|
+
const e = await err;
|
|
50316
|
+
if (e) return fail(`Couldn't assign task ${taskId}: ${e.message}`);
|
|
50170
50317
|
return ok(`Assigned task ${taskId} to @${to}.`);
|
|
50171
50318
|
})
|
|
50172
50319
|
);
|
|
@@ -50264,7 +50411,7 @@ app.use((_req, res, next) => {
|
|
|
50264
50411
|
next();
|
|
50265
50412
|
});
|
|
50266
50413
|
app.use(import_express.default.json({ limit: "25mb" }));
|
|
50267
|
-
var health = (_req, res) => res.json({ ok: true, service: "
|
|
50414
|
+
var health = (_req, res) => res.json({ ok: true, service: "jefrichat-mcp-http", hub: HUB, sessions: sessions.size });
|
|
50268
50415
|
app.get("/health", health);
|
|
50269
50416
|
app.get("/mcp/health", health);
|
|
50270
50417
|
app.post("/mcp", async (req, res) => {
|
package/dist/index.js
CHANGED
|
@@ -24813,6 +24813,9 @@ import { webcrypto } from "node:crypto";
|
|
|
24813
24813
|
function dmConversationId(a, b) {
|
|
24814
24814
|
return "dm:" + [a, b].sort().join(":");
|
|
24815
24815
|
}
|
|
24816
|
+
function groupConversationId(groupId) {
|
|
24817
|
+
return "group:" + groupId;
|
|
24818
|
+
}
|
|
24816
24819
|
|
|
24817
24820
|
// ../sdk/src/index.ts
|
|
24818
24821
|
var subtle = webcrypto.subtle;
|
|
@@ -24943,7 +24946,7 @@ var AcpClient = class _AcpClient {
|
|
|
24943
24946
|
});
|
|
24944
24947
|
});
|
|
24945
24948
|
}
|
|
24946
|
-
// Keep the hub connection alive through idle-timeout proxies
|
|
24949
|
+
// Keep the hub connection alive through idle-timeout proxies.
|
|
24947
24950
|
startPing() {
|
|
24948
24951
|
this.stopPing();
|
|
24949
24952
|
this.pingTimer = setInterval(() => {
|
|
@@ -25305,10 +25308,13 @@ var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octe
|
|
|
25305
25308
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
25306
25309
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
25307
25310
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
25308
|
-
async function uploadFileHttp(c, serverUrl,
|
|
25311
|
+
async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
|
|
25309
25312
|
const hub = serverUrl.replace(/\/$/, "");
|
|
25310
|
-
const
|
|
25311
|
-
|
|
25313
|
+
const params = new URLSearchParams({ fileName });
|
|
25314
|
+
if (target.to) params.set("to", target.to);
|
|
25315
|
+
if (target.groupId) params.set("groupId", target.groupId);
|
|
25316
|
+
if (caption) params.set("caption", caption);
|
|
25317
|
+
const res = await fetch(`${hub}/api/files?${params.toString()}`, {
|
|
25312
25318
|
method: "POST",
|
|
25313
25319
|
headers: { Authorization: `Bearer ${c.token}`, "Content-Type": mime },
|
|
25314
25320
|
body: Buffer.from(bytes)
|
|
@@ -25336,6 +25342,8 @@ var ok = (text) => ({ content: [{ type: "text", text }] });
|
|
|
25336
25342
|
var fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
25337
25343
|
var inboxWatermark = /* @__PURE__ */ new Map();
|
|
25338
25344
|
function registerAcpTools(server2, ctx) {
|
|
25345
|
+
const ENABLE_E2E = process.env.ACP_ENABLE_E2E === "true";
|
|
25346
|
+
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
25339
25347
|
const withClient = async (fn) => {
|
|
25340
25348
|
try {
|
|
25341
25349
|
const c = await ctx.ensureClient();
|
|
@@ -25389,7 +25397,7 @@ ${e?.message ?? e}`);
|
|
|
25389
25397
|
return ok(`Sent to @${to}: "${text}"`);
|
|
25390
25398
|
})
|
|
25391
25399
|
);
|
|
25392
|
-
|
|
25400
|
+
e2eServer.registerTool(
|
|
25393
25401
|
"jefri_send_private",
|
|
25394
25402
|
{
|
|
25395
25403
|
title: "Send a private (client-encrypted) message",
|
|
@@ -25407,7 +25415,7 @@ ${e?.message ?? e}`);
|
|
|
25407
25415
|
return ok(`Sent private E2E message to @${to}.`);
|
|
25408
25416
|
})
|
|
25409
25417
|
);
|
|
25410
|
-
|
|
25418
|
+
e2eServer.registerTool(
|
|
25411
25419
|
"jefri_e2e_fingerprint",
|
|
25412
25420
|
{
|
|
25413
25421
|
title: "Show E2E key fingerprint",
|
|
@@ -25420,7 +25428,7 @@ ${e?.message ?? e}`);
|
|
|
25420
25428
|
${fp}`);
|
|
25421
25429
|
})
|
|
25422
25430
|
);
|
|
25423
|
-
|
|
25431
|
+
e2eServer.registerTool(
|
|
25424
25432
|
"jefri_send_private_file",
|
|
25425
25433
|
{
|
|
25426
25434
|
title: "Send a private (client-encrypted) file",
|
|
@@ -25436,10 +25444,15 @@ ${fp}`);
|
|
|
25436
25444
|
async ({ to, path: path3, dataUrl, fileName, caption }) => withClient(async (c) => {
|
|
25437
25445
|
let name, mime, url;
|
|
25438
25446
|
if (path3) {
|
|
25439
|
-
|
|
25447
|
+
if (!ctx.local) {
|
|
25448
|
+
return fail(
|
|
25449
|
+
`I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
|
|
25450
|
+
);
|
|
25451
|
+
}
|
|
25452
|
+
const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
|
|
25440
25453
|
if (!fs2.existsSync(abs)) {
|
|
25441
25454
|
return fail(
|
|
25442
|
-
`I can't
|
|
25455
|
+
`I can't find a file at ${path3} on this machine.`
|
|
25443
25456
|
);
|
|
25444
25457
|
}
|
|
25445
25458
|
const buf = fs2.readFileSync(abs);
|
|
@@ -25458,7 +25471,7 @@ ${fp}`);
|
|
|
25458
25471
|
return ok(`Sent private E2E file to @${to}.`);
|
|
25459
25472
|
})
|
|
25460
25473
|
);
|
|
25461
|
-
|
|
25474
|
+
e2eServer.registerTool(
|
|
25462
25475
|
"jefri_send_private_group",
|
|
25463
25476
|
{
|
|
25464
25477
|
title: "Send a private (client-encrypted) group message",
|
|
@@ -25476,7 +25489,7 @@ ${fp}`);
|
|
|
25476
25489
|
return ok(`Sent private E2E group message.`);
|
|
25477
25490
|
})
|
|
25478
25491
|
);
|
|
25479
|
-
|
|
25492
|
+
e2eServer.registerTool(
|
|
25480
25493
|
"jefri_send_private_group_file",
|
|
25481
25494
|
{
|
|
25482
25495
|
title: "Send a private (client-encrypted) group file",
|
|
@@ -25492,10 +25505,15 @@ ${fp}`);
|
|
|
25492
25505
|
async ({ groupId, path: path3, dataUrl, fileName, caption }) => withClient(async (c) => {
|
|
25493
25506
|
let name, mime, url;
|
|
25494
25507
|
if (path3) {
|
|
25495
|
-
|
|
25508
|
+
if (!ctx.local) {
|
|
25509
|
+
return fail(
|
|
25510
|
+
`I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
|
|
25511
|
+
);
|
|
25512
|
+
}
|
|
25513
|
+
const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
|
|
25496
25514
|
if (!fs2.existsSync(abs)) {
|
|
25497
25515
|
return fail(
|
|
25498
|
-
`I can't
|
|
25516
|
+
`I can't find a file at ${path3} on this machine.`
|
|
25499
25517
|
);
|
|
25500
25518
|
}
|
|
25501
25519
|
const buf = fs2.readFileSync(abs);
|
|
@@ -25530,25 +25548,32 @@ ${fp}`);
|
|
|
25530
25548
|
async ({ to, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
25531
25549
|
let name, mime, url;
|
|
25532
25550
|
if (path3) {
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
const fname = np.basename(abs);
|
|
25551
|
+
if (!ctx.local) {
|
|
25552
|
+
const fname = np.basename(path3);
|
|
25536
25553
|
const fmime = mimeOf(fname);
|
|
25537
25554
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
25538
25555
|
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
25539
25556
|
const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
|
|
25540
25557
|
const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
25541
|
-
const
|
|
25558
|
+
const uploadUrl = `${hub}/api/files?${q}`;
|
|
25559
|
+
const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
|
|
25560
|
+
const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path3}"`;
|
|
25542
25561
|
return fail(
|
|
25543
|
-
`
|
|
25562
|
+
`ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
|
|
25563
|
+
|
|
25564
|
+
\u2022 macOS / Linux:
|
|
25565
|
+
${cmd}
|
|
25544
25566
|
|
|
25545
|
-
\
|
|
25546
|
-
${
|
|
25567
|
+
\u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
|
|
25568
|
+
${psCmd}
|
|
25547
25569
|
|
|
25548
|
-
|
|
25549
|
-
${cmd}`
|
|
25570
|
+
(Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
|
|
25550
25571
|
);
|
|
25551
25572
|
}
|
|
25573
|
+
const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
|
|
25574
|
+
if (!fs2.existsSync(abs)) {
|
|
25575
|
+
return fail(`I can't find a file at ${path3} on this machine.`);
|
|
25576
|
+
}
|
|
25552
25577
|
const buf = fs2.readFileSync(abs);
|
|
25553
25578
|
name = np.basename(abs);
|
|
25554
25579
|
mime = mimeOf(name);
|
|
@@ -25565,7 +25590,7 @@ ${cmd}`
|
|
|
25565
25590
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
25566
25591
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
25567
25592
|
try {
|
|
25568
|
-
await uploadFileHttp(c, ctx.serverUrl, to, name, mime, rawBytes, caption);
|
|
25593
|
+
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
25569
25594
|
} catch (e2) {
|
|
25570
25595
|
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
25571
25596
|
}
|
|
@@ -25599,7 +25624,7 @@ ${cmd}`
|
|
|
25599
25624
|
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
25600
25625
|
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
25601
25626
|
try {
|
|
25602
|
-
await uploadFileHttp(c, ctx.serverUrl, to, fileName, mime, contentBytes, caption);
|
|
25627
|
+
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
25603
25628
|
} catch (e2) {
|
|
25604
25629
|
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
25605
25630
|
}
|
|
@@ -25631,7 +25656,7 @@ ${cmd}`
|
|
|
25631
25656
|
`Sending a folder reads files off disk, which this connector can't do \u2014 it runs in the cloud. Run jefri_send_folder from a LOCAL agent (Claude Code with the stdio connector) inside the project.`
|
|
25632
25657
|
);
|
|
25633
25658
|
const rawDir = path3 && path3.trim() ? path3 : process.cwd();
|
|
25634
|
-
const dir = rawDir.startsWith("~") ? np.join(
|
|
25659
|
+
const dir = rawDir.startsWith("~") ? np.join(os2.homedir(), rawDir.slice(1)) : rawDir;
|
|
25635
25660
|
if (!fs2.existsSync(dir))
|
|
25636
25661
|
return fail(
|
|
25637
25662
|
`I can't see "${rawDir}" from here. Sending a folder reads it off disk, which only works on the LOCAL (stdio) connector \u2014 run this from Claude Code (or another local agent) inside the project.`
|
|
@@ -25716,27 +25741,42 @@ ${cmd}`
|
|
|
25716
25741
|
"jefri_download_file",
|
|
25717
25742
|
{
|
|
25718
25743
|
title: "Download / save a file someone sent you",
|
|
25719
|
-
description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine.
|
|
25744
|
+
description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine. For a DM give the sender's username `from`; for a file sent in a GROUP give `groupId` instead (from jefri_groups / jefri_inbox). Add `fileName` if there are several. Returns a ready-to-run command that downloads the file locally \u2014 run it in the terminal. Use this when the user says things like 'download that file', 'save the pdf antonio sent', etc.",
|
|
25720
25745
|
inputSchema: {
|
|
25721
|
-
from: external_exports.string().describe("who sent you the file (their username)"),
|
|
25722
|
-
|
|
25746
|
+
from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
|
|
25747
|
+
groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
|
|
25748
|
+
fileName: external_exports.string().optional().describe("which file, if there are several"),
|
|
25723
25749
|
savePath: external_exports.string().optional().describe("where to save it (default: the file's own name in the current folder)")
|
|
25724
25750
|
}
|
|
25725
25751
|
},
|
|
25726
|
-
async ({ from, fileName, savePath }) => withClient(async (c) => {
|
|
25727
|
-
|
|
25752
|
+
async ({ from, groupId, fileName, savePath }) => withClient(async (c) => {
|
|
25753
|
+
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
25754
|
+
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
|
|
25755
|
+
const where = groupId ? "this group" : `@${from}`;
|
|
25728
25756
|
const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
|
|
25729
25757
|
c.history(convId);
|
|
25730
25758
|
const res = await p;
|
|
25731
25759
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
25732
|
-
if (!files.length) return fail(`No files
|
|
25760
|
+
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
25733
25761
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
25734
|
-
if (!match) return fail(`No file named "${fileName}"
|
|
25762
|
+
if (!match) return fail(`No file named "${fileName}" in ${where}.`);
|
|
25735
25763
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
25736
25764
|
const out = savePath || match.fileName || "downloaded.file";
|
|
25765
|
+
if (ctx.local) {
|
|
25766
|
+
const outPath = out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
|
|
25767
|
+
try {
|
|
25768
|
+
const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
|
|
25769
|
+
if (!r.ok) throw new Error(`download failed (${r.status})`);
|
|
25770
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
25771
|
+
fs2.writeFileSync(outPath, buf);
|
|
25772
|
+
return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
|
|
25773
|
+
} catch (e) {
|
|
25774
|
+
return fail(`Couldn't save the file: ${e?.message ?? e}`);
|
|
25775
|
+
}
|
|
25776
|
+
}
|
|
25737
25777
|
const cmd = `curl -s -o ${shq(out)} -H "Authorization: Bearer ${c.token}" ${shq(`${hub}/api/files/${match.id}`)}`;
|
|
25738
25778
|
return ok(
|
|
25739
|
-
`To save \u{1F4CE} ${match.fileName} from
|
|
25779
|
+
`To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
|
|
25740
25780
|
|
|
25741
25781
|
${cmd}
|
|
25742
25782
|
|
|
@@ -25984,24 +26024,129 @@ ${cmd}`);
|
|
|
25984
26024
|
if (!msgs.length) return ok("\u{1F4ED} No new messages.");
|
|
25985
26025
|
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
|
|
25986
26026
|
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
26027
|
+
const groupNames = /* @__PURE__ */ new Map();
|
|
26028
|
+
if (msgs.some((m) => m.groupId)) {
|
|
26029
|
+
try {
|
|
26030
|
+
for (const g of await c.groups()) groupNames.set(g.id, g.name);
|
|
26031
|
+
} catch {
|
|
26032
|
+
}
|
|
26033
|
+
}
|
|
26034
|
+
const where = (m) => {
|
|
26035
|
+
if (!m.groupId) return "";
|
|
26036
|
+
const name = groupNames.get(m.groupId);
|
|
26037
|
+
return name ? ` (in group "${name}" \xB7 reply with jefri_send_group groupId="${m.groupId}")` : ` (in group id ${m.groupId})`;
|
|
26038
|
+
};
|
|
25987
26039
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
25988
26040
|
if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
|
|
25989
26041
|
try {
|
|
25990
26042
|
const plain = await c.decryptPrivatePayload(m.encryptedPayload);
|
|
25991
26043
|
const body2 = plain.kind === "file" ? `\u{1F512}\u{1F4CE} private file: ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`;
|
|
25992
|
-
return `@${m.senderUsername}${m
|
|
26044
|
+
return `@${m.senderUsername}${where(m)}: ${body2}`;
|
|
25993
26045
|
} catch {
|
|
25994
|
-
return `@${m.senderUsername}${m
|
|
26046
|
+
return `@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
|
|
25995
26047
|
}
|
|
25996
26048
|
}
|
|
25997
26049
|
const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
|
|
25998
|
-
|
|
25999
|
-
return `@${m.senderUsername}${where}: ${body}`;
|
|
26050
|
+
return `@${m.senderUsername}${where(m)}: ${body}`;
|
|
26000
26051
|
}));
|
|
26001
26052
|
return ok(`\u{1F4E8} ${lines.length} message(s):
|
|
26002
26053
|
` + lines.join("\n"));
|
|
26003
26054
|
})
|
|
26004
26055
|
);
|
|
26056
|
+
server2.registerTool(
|
|
26057
|
+
"jefri_groups",
|
|
26058
|
+
{
|
|
26059
|
+
title: "List your groups",
|
|
26060
|
+
description: "List the groups you're a member of, with each group's id (needed to post to it) and its members. Use this to find a group's id before jefri_send_group.",
|
|
26061
|
+
inputSchema: {}
|
|
26062
|
+
},
|
|
26063
|
+
async () => withClient(async (c) => {
|
|
26064
|
+
const gs = await c.groups();
|
|
26065
|
+
if (!gs.length) return ok("You're not in any groups yet.");
|
|
26066
|
+
return ok(
|
|
26067
|
+
`You're in ${gs.length} group(s):
|
|
26068
|
+
` + gs.map((g) => `\u2022 "${g.name}" \u2014 id: ${g.id} \u2014 ${g.memberUsernames.length} members: ${g.memberUsernames.join(", ")}`).join("\n")
|
|
26069
|
+
);
|
|
26070
|
+
})
|
|
26071
|
+
);
|
|
26072
|
+
server2.registerTool(
|
|
26073
|
+
"jefri_send_group",
|
|
26074
|
+
{
|
|
26075
|
+
title: "Send a group message",
|
|
26076
|
+
description: "Post a message to a GROUP (everyone in the group sees it), by the group's id. Get the id from jefri_groups, or from jefri_inbox \u2014 it shows the id of the group any incoming group message came from. NOTE: this needs the group's id, not its display name.",
|
|
26077
|
+
inputSchema: {
|
|
26078
|
+
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
26079
|
+
text: external_exports.string().describe("the message to post to the group")
|
|
26080
|
+
}
|
|
26081
|
+
},
|
|
26082
|
+
async ({ groupId, text }) => withClient(async (c) => {
|
|
26083
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
26084
|
+
c.groupMessage(groupId, text);
|
|
26085
|
+
const e = await err;
|
|
26086
|
+
if (e) return fail(`Couldn't post to the group: ${e.message}`);
|
|
26087
|
+
return ok(`Posted to the group.`);
|
|
26088
|
+
})
|
|
26089
|
+
);
|
|
26090
|
+
server2.registerTool(
|
|
26091
|
+
"jefri_send_group_file",
|
|
26092
|
+
{
|
|
26093
|
+
title: "Send a file to a group",
|
|
26094
|
+
description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
|
|
26095
|
+
inputSchema: {
|
|
26096
|
+
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
26097
|
+
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
26098
|
+
caption: external_exports.string().optional().describe("optional text caption"),
|
|
26099
|
+
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
26100
|
+
fileName: external_exports.string().optional().describe("file name when using dataUrl")
|
|
26101
|
+
}
|
|
26102
|
+
},
|
|
26103
|
+
async ({ groupId, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
26104
|
+
let name, mime, url;
|
|
26105
|
+
if (path3) {
|
|
26106
|
+
if (!ctx.local) {
|
|
26107
|
+
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
26108
|
+
const fname = np.basename(path3);
|
|
26109
|
+
const fmime = mimeOf(fname);
|
|
26110
|
+
const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
26111
|
+
const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
|
|
26112
|
+
return fail(
|
|
26113
|
+
`ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
|
|
26114
|
+
|
|
26115
|
+
${cmd}`
|
|
26116
|
+
);
|
|
26117
|
+
}
|
|
26118
|
+
const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
|
|
26119
|
+
if (!fs2.existsSync(abs)) return fail(`I can't find a file at ${path3} on this machine.`);
|
|
26120
|
+
const buf = fs2.readFileSync(abs);
|
|
26121
|
+
name = np.basename(abs);
|
|
26122
|
+
mime = mimeOf(name);
|
|
26123
|
+
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
26124
|
+
} else if (dataUrl && fileName) {
|
|
26125
|
+
name = fileName;
|
|
26126
|
+
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
26127
|
+
url = dataUrl;
|
|
26128
|
+
} else {
|
|
26129
|
+
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
26130
|
+
}
|
|
26131
|
+
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
26132
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
26133
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
26134
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
26135
|
+
try {
|
|
26136
|
+
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
26137
|
+
} catch (e2) {
|
|
26138
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
26139
|
+
}
|
|
26140
|
+
return ok(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
|
|
26141
|
+
}
|
|
26142
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
26143
|
+
c.sendGroupFile(groupId, name, mime, url);
|
|
26144
|
+
if (caption) c.groupMessage(groupId, caption);
|
|
26145
|
+
const e = await err;
|
|
26146
|
+
if (e) return fail(`Rejected: ${e.message}`);
|
|
26147
|
+
return ok(`Sent \u{1F4CE} ${name} to the group.`);
|
|
26148
|
+
})
|
|
26149
|
+
);
|
|
26005
26150
|
server2.registerTool(
|
|
26006
26151
|
"jefri_history",
|
|
26007
26152
|
{
|
|
@@ -26073,8 +26218,10 @@ ${cmd}`);
|
|
|
26073
26218
|
inputSchema: { taskId: external_exports.string(), to: external_exports.string() }
|
|
26074
26219
|
},
|
|
26075
26220
|
async ({ taskId, to }) => withClient(async (c) => {
|
|
26221
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
26076
26222
|
c.assignTask(taskId, to);
|
|
26077
|
-
|
|
26223
|
+
const e = await err;
|
|
26224
|
+
if (e) return fail(`Couldn't assign task ${taskId}: ${e.message}`);
|
|
26078
26225
|
return ok(`Assigned task ${taskId} to @${to}.`);
|
|
26079
26226
|
})
|
|
26080
26227
|
);
|
package/package.json
CHANGED