jefrichat-mcp 0.48.13 → 0.49.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/dist/http.js +153 -22
- package/dist/index.js +574 -171
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -60904,6 +60904,22 @@ var JefriAuthError = class extends Error {
|
|
|
60904
60904
|
var JefriClient = class _JefriClient {
|
|
60905
60905
|
ws;
|
|
60906
60906
|
handlers = /* @__PURE__ */ new Map();
|
|
60907
|
+
// Round 2 of the sessions audit (finding #1): a message can arrive in the
|
|
60908
|
+
// milliseconds between `registered` and the caller attaching its handlers —
|
|
60909
|
+
// connect() itself awaits E2E setup after registration, and the connector
|
|
60910
|
+
// wires its inbox after connect() resolves. Events nobody is listening for
|
|
60911
|
+
// yet are BUFFERED (delivery-critical types only) and replayed, in order,
|
|
60912
|
+
// to the first subscriber. Live listeners bypass the buffer entirely.
|
|
60913
|
+
static REPLAYABLE = /* @__PURE__ */ new Set([
|
|
60914
|
+
"message_received",
|
|
60915
|
+
"file_received",
|
|
60916
|
+
"debate_turn",
|
|
60917
|
+
"debate_summary_request",
|
|
60918
|
+
"debate_cancel"
|
|
60919
|
+
]);
|
|
60920
|
+
static REPLAY_MAX = 500;
|
|
60921
|
+
replayBuffer = [];
|
|
60922
|
+
replayScheduled = false;
|
|
60907
60923
|
server;
|
|
60908
60924
|
token;
|
|
60909
60925
|
identity;
|
|
@@ -61160,14 +61176,33 @@ var JefriClient = class _JefriClient {
|
|
|
61160
61176
|
const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
61161
61177
|
set.add(handler);
|
|
61162
61178
|
this.handlers.set(event, set);
|
|
61179
|
+
if (this.replayBuffer.length && _JefriClient.REPLAYABLE.has(event)) this.scheduleReplayDrain();
|
|
61163
61180
|
return this;
|
|
61164
61181
|
}
|
|
61165
61182
|
off(event, handler) {
|
|
61166
61183
|
this.handlers.get(event)?.delete(handler);
|
|
61167
61184
|
return this;
|
|
61168
61185
|
}
|
|
61169
|
-
|
|
61170
|
-
|
|
61186
|
+
scheduleReplayDrain() {
|
|
61187
|
+
if (this.replayScheduled) return;
|
|
61188
|
+
this.replayScheduled = true;
|
|
61189
|
+
queueMicrotask(() => {
|
|
61190
|
+
this.replayScheduled = false;
|
|
61191
|
+
const drain3 = this.replayBuffer;
|
|
61192
|
+
this.replayBuffer = [];
|
|
61193
|
+
const keep = [];
|
|
61194
|
+
for (const b of drain3) {
|
|
61195
|
+
const hs = this.handlers.get(b.event);
|
|
61196
|
+
if (hs?.size) this.dispatch(b.event, b.payload, hs);
|
|
61197
|
+
else keep.push(b);
|
|
61198
|
+
}
|
|
61199
|
+
this.replayBuffer = keep.concat(this.replayBuffer);
|
|
61200
|
+
});
|
|
61201
|
+
}
|
|
61202
|
+
/** emit()'s containment, shared with replay: a throwing handler must NEVER
|
|
61203
|
+
* propagate or starve the others; async rejections are contained too. */
|
|
61204
|
+
dispatch(event, payload, handlers) {
|
|
61205
|
+
for (const h of handlers) {
|
|
61171
61206
|
try {
|
|
61172
61207
|
const res = h(payload);
|
|
61173
61208
|
if (res instanceof Promise)
|
|
@@ -61177,6 +61212,16 @@ var JefriClient = class _JefriClient {
|
|
|
61177
61212
|
}
|
|
61178
61213
|
}
|
|
61179
61214
|
}
|
|
61215
|
+
emit(event, payload) {
|
|
61216
|
+
if (_JefriClient.REPLAYABLE.has(event) && !this.handlers.get(event)?.size) {
|
|
61217
|
+
if (this.replayBuffer.length >= _JefriClient.REPLAY_MAX) {
|
|
61218
|
+
this.replayBuffer.shift();
|
|
61219
|
+
console.error(`[jefri-sdk] replay buffer overflow for pre-subscription events \u2014 oldest dropped`);
|
|
61220
|
+
}
|
|
61221
|
+
this.replayBuffer.push({ event, payload });
|
|
61222
|
+
}
|
|
61223
|
+
this.dispatch(event, payload, this.handlers.get(event) ?? []);
|
|
61224
|
+
}
|
|
61180
61225
|
send(ev) {
|
|
61181
61226
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN)
|
|
61182
61227
|
throw new Error("not connected to the Jefri Chat hub (reconnecting) \u2014 try again in a moment");
|
|
@@ -62114,9 +62159,25 @@ function declKey(extra, parts) {
|
|
|
62114
62159
|
return "d-" + createHash2("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
62115
62160
|
}
|
|
62116
62161
|
var crossBasename = (p) => String(p).split(/[\\/]/).pop() || "file";
|
|
62162
|
+
var DOWNLOAD_DIR_SH = "~/Downloads/jefri";
|
|
62163
|
+
var downloadDirAbs = () => process.env.JEFRI_DOWNLOAD_DIR || np3.join(os4.homedir(), "Downloads", "jefri");
|
|
62164
|
+
var ensureDownloadDir = () => {
|
|
62165
|
+
const dir = downloadDirAbs();
|
|
62166
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
62167
|
+
const st = fs4.lstatSync(dir);
|
|
62168
|
+
if (st.isSymbolicLink() || !st.isDirectory())
|
|
62169
|
+
throw new Error(`refusing to save: ${dir} is a symlink (or not a directory) \u2014 a planted link could redirect downloads anywhere. Remove it and retry.`);
|
|
62170
|
+
return dir;
|
|
62171
|
+
};
|
|
62172
|
+
var safeSaveName = (p) => {
|
|
62173
|
+
const base = String(p ?? "").split(/[\\/]/).pop() ?? "";
|
|
62174
|
+
const clean = base.replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
62175
|
+
return !clean || clean === "." || clean === ".." ? "" : clean;
|
|
62176
|
+
};
|
|
62117
62177
|
var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
|
|
62118
62178
|
var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
62119
62179
|
var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
|
|
62180
|
+
var isTextyFile = (mime, name) => /^text\/|json|xml|javascript|typescript|markdown|csv|ya?ml|x-sh|toml/i.test(mime) || /\.(md|txt|json|csv|ya?ml|log|ts|tsx|js|jsx|py|rb|go|rs|java|c|h|cpp|sh|sql|toml|env|cfg|ini)$/i.test(name);
|
|
62120
62181
|
var DATAURL_INLINE_MAX = 256 * 1024;
|
|
62121
62182
|
var dataUrlTooBig = (dataUrl) => dataUrl.length > DATAURL_INLINE_MAX * 1.4 ? `That dataUrl is ~${MB(dataUrl.length * 0.75)} MB decoded \u2014 far past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. NEVER read a local file and base64 it through the model. Call this tool again with the file's PATH \u2014 you'll get a single-use upload command that sends it in seconds.` : null;
|
|
62122
62183
|
async function remoteUploadCommand(c, ctx, target, path3, caption, where) {
|
|
@@ -62519,9 +62580,13 @@ ${e?.message ?? e}`);
|
|
|
62519
62580
|
description: "Show this session's Jefri Chat identity (username, display name) and connection status.",
|
|
62520
62581
|
inputSchema: {}
|
|
62521
62582
|
},
|
|
62522
|
-
async () =>
|
|
62523
|
-
|
|
62524
|
-
|
|
62583
|
+
async () => {
|
|
62584
|
+
const unbound = await ctx.identityStatus?.();
|
|
62585
|
+
if (unbound) return ok3(unbound);
|
|
62586
|
+
return withClient(
|
|
62587
|
+
async (c) => ok3(`You are "${c.identity.displayName}" (@${c.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
|
|
62588
|
+
);
|
|
62589
|
+
}
|
|
62525
62590
|
);
|
|
62526
62591
|
server2.registerTool(
|
|
62527
62592
|
"jefri_agents",
|
|
@@ -63004,13 +63069,13 @@ ${fp}`);
|
|
|
63004
63069
|
server2.registerTool(
|
|
63005
63070
|
"jefri_download_file",
|
|
63006
63071
|
{
|
|
63007
|
-
title: "
|
|
63008
|
-
description: "
|
|
63072
|
+
title: "Read or save a file someone sent you",
|
|
63073
|
+
description: "Get a file another agent/user sent you. For a DM give the sender's username `from`; for a GROUP file give `groupId` (from jefri_groups / jefri_inbox). Add `fileName` if there are several. TEXT files (code, markdown, JSON, CSV, logs \u2014 up to 256KB) come back with their FULL CONTENT inline, and PDFs come back as EXTRACTED TEXT \u2014 so you can read and discuss both directly, no terminal needed. Only images, video and other binaries return a save command (or view them in the web app). On the local connector, files are saved straight to disk. Use this when the user says 'read the file sara sent', 'download that file', 'what does the doc say', etc.",
|
|
63009
63074
|
inputSchema: {
|
|
63010
63075
|
from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
|
|
63011
63076
|
groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
|
|
63012
63077
|
fileName: external_exports.string().optional().describe("which file, if there are several"),
|
|
63013
|
-
savePath: external_exports.string().optional().describe("
|
|
63078
|
+
savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 every download lands in ~/Downloads/jefri/, never elsewhere); a taken name gets a -1/-2 suffix")
|
|
63014
63079
|
}
|
|
63015
63080
|
},
|
|
63016
63081
|
async ({ from, groupId, fileName, savePath }) => withClient(async (c) => {
|
|
@@ -63023,26 +63088,92 @@ ${fp}`);
|
|
|
63023
63088
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
63024
63089
|
if (!match) return fail(`No file named "${fileName}" in ${where}.`);
|
|
63025
63090
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
63026
|
-
const
|
|
63091
|
+
const safeName2 = safeSaveName(match.fileName) || "downloaded.file";
|
|
63092
|
+
const out = safeSaveName(savePath) || safeName2;
|
|
63027
63093
|
if (ctx.local) {
|
|
63028
|
-
const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
|
|
63029
63094
|
try {
|
|
63095
|
+
const dlDir = ensureDownloadDir();
|
|
63096
|
+
const outPath = np3.join(dlDir, out);
|
|
63030
63097
|
const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
|
|
63031
63098
|
if (!r.ok) throw new Error(`download failed (${r.status})`);
|
|
63032
63099
|
const buf = Buffer.from(await r.arrayBuffer());
|
|
63033
|
-
|
|
63034
|
-
|
|
63100
|
+
let finalPath = outPath;
|
|
63101
|
+
const ext = np3.extname(finalPath);
|
|
63102
|
+
const stem = finalPath.slice(0, finalPath.length - ext.length);
|
|
63103
|
+
let written = false;
|
|
63104
|
+
for (let i = 0; i < 100 && !written; i++) {
|
|
63105
|
+
finalPath = i === 0 ? outPath : `${stem}-${i}${ext}`;
|
|
63106
|
+
try {
|
|
63107
|
+
fs4.writeFileSync(finalPath, buf, { flag: "wx" });
|
|
63108
|
+
written = true;
|
|
63109
|
+
} catch (e) {
|
|
63110
|
+
if (e?.code !== "EEXIST") throw e;
|
|
63111
|
+
}
|
|
63112
|
+
}
|
|
63113
|
+
if (!written) return fail(`Couldn't save: 100 files named like ${outPath} already exist here.`);
|
|
63114
|
+
return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${finalPath} (${MB(buf.length)} MB).`);
|
|
63035
63115
|
} catch (e) {
|
|
63036
63116
|
return fail(`Couldn't save the file: ${e?.message ?? e}`);
|
|
63037
63117
|
}
|
|
63038
63118
|
}
|
|
63039
|
-
const
|
|
63119
|
+
const fname2 = safeName2;
|
|
63120
|
+
const fmime2 = match.fileMime || mimeOf(fname2);
|
|
63121
|
+
const sizeKnownTooBig = typeof match.fileSize === "number" && match.fileSize > DATAURL_INLINE_MAX;
|
|
63122
|
+
if (isTextyFile(fmime2, fname2) && !sizeKnownTooBig) {
|
|
63123
|
+
try {
|
|
63124
|
+
const ac = new AbortController();
|
|
63125
|
+
const killer = setTimeout(() => ac.abort(), 15e3);
|
|
63126
|
+
try {
|
|
63127
|
+
const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` }, signal: ac.signal });
|
|
63128
|
+
if (r.ok && r.body) {
|
|
63129
|
+
const declared = Number(r.headers.get("content-length") ?? 0);
|
|
63130
|
+
if (!declared || declared <= DATAURL_INLINE_MAX) {
|
|
63131
|
+
const bytes = await readCapped(r.body, DATAURL_INLINE_MAX, Date.now() + 15e3);
|
|
63132
|
+
return ok3(
|
|
63133
|
+
`\u{1F4CE} ${fname2} from ${where} (${(bytes.length / 1024).toFixed(1)} KB) \u2014 full content:
|
|
63134
|
+
|
|
63135
|
+
` + bytes.toString("utf8")
|
|
63136
|
+
);
|
|
63137
|
+
}
|
|
63138
|
+
}
|
|
63139
|
+
} finally {
|
|
63140
|
+
clearTimeout(killer);
|
|
63141
|
+
}
|
|
63142
|
+
} catch {
|
|
63143
|
+
}
|
|
63144
|
+
}
|
|
63145
|
+
if (/pdf$/i.test(fmime2) || /\.pdf$/i.test(fname2)) {
|
|
63146
|
+
const pdfTooBig = typeof match.fileSize === "number" && match.fileSize > 15 * 1024 * 1024;
|
|
63147
|
+
if (!pdfTooBig) {
|
|
63148
|
+
try {
|
|
63149
|
+
const ac = new AbortController();
|
|
63150
|
+
const killer = setTimeout(() => ac.abort(), 3e4);
|
|
63151
|
+
try {
|
|
63152
|
+
const r = await fetch(`${hub}/api/files/${match.id}/text`, { headers: { authorization: `Bearer ${c.token}` }, signal: ac.signal });
|
|
63153
|
+
if (r.ok) {
|
|
63154
|
+
const body = await r.json();
|
|
63155
|
+
if (body?.text)
|
|
63156
|
+
return ok3(
|
|
63157
|
+
`\u{1F4CE} ${fname2} from ${where} \u2014 extracted text (${body.chars ?? body.text.length} chars):
|
|
63158
|
+
|
|
63159
|
+
` + body.text
|
|
63160
|
+
);
|
|
63161
|
+
}
|
|
63162
|
+
} finally {
|
|
63163
|
+
clearTimeout(killer);
|
|
63164
|
+
}
|
|
63165
|
+
} catch {
|
|
63166
|
+
}
|
|
63167
|
+
}
|
|
63168
|
+
}
|
|
63169
|
+
const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${out}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
|
|
63170
|
+
const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
63040
63171
|
return ok3(
|
|
63041
|
-
|
|
63042
|
-
|
|
63043
|
-
${cmd}
|
|
63172
|
+
`\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
|
|
63044
63173
|
|
|
63045
|
-
|
|
63174
|
+
\u2022 Easiest: open the chat in the web app, the file renders there: ${web2}
|
|
63175
|
+
\u2022 Terminal: ${cmd}
|
|
63176
|
+
(set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`
|
|
63046
63177
|
);
|
|
63047
63178
|
})
|
|
63048
63179
|
);
|
|
@@ -63097,21 +63228,21 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
63097
63228
|
inputSchema: {
|
|
63098
63229
|
departmentId: external_exports.string(),
|
|
63099
63230
|
fileId: external_exports.string(),
|
|
63100
|
-
savePath: external_exports.string().optional().describe("
|
|
63231
|
+
savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 the command saves into ~/Downloads/jefri/)")
|
|
63101
63232
|
}
|
|
63102
63233
|
},
|
|
63103
63234
|
async ({ departmentId, fileId, savePath }) => withClient(async (c) => {
|
|
63104
|
-
let name = savePath || "department-file";
|
|
63235
|
+
let name = safeSaveName(savePath) || "department-file";
|
|
63105
63236
|
try {
|
|
63106
63237
|
const r = await fetch(`${hubBase()}/api/departments/${departmentId}/drive`, { headers: { authorization: `Bearer ${c.token}` } });
|
|
63107
63238
|
if (r.ok) {
|
|
63108
63239
|
const { files } = await r.json();
|
|
63109
63240
|
const f = files.find((x) => x.id === fileId);
|
|
63110
|
-
if (f && !savePath) name = f.name;
|
|
63241
|
+
if (f && !safeSaveName(savePath)) name = safeSaveName(f.name) || name;
|
|
63111
63242
|
}
|
|
63112
63243
|
} catch {
|
|
63113
63244
|
}
|
|
63114
|
-
const cmd = `curl -s -o ${
|
|
63245
|
+
const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${name}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
|
|
63115
63246
|
return ok3(`To save this department file, run in the terminal:
|
|
63116
63247
|
|
|
63117
63248
|
${cmd}
|
|
@@ -63201,7 +63332,7 @@ ${h.text}`).join("\n\n---\n\n")
|
|
|
63201
63332
|
if (d.text) return ok3(`Contents of "${d.name}":
|
|
63202
63333
|
|
|
63203
63334
|
${d.text}`);
|
|
63204
|
-
const cmd = `curl -s -o ${
|
|
63335
|
+
const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${safeSaveName(d.name) || "document"}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
|
|
63205
63336
|
return ok3(`"${d.name}" is a ${d.mime} file. To download it and work with it locally, run in the terminal:
|
|
63206
63337
|
|
|
63207
63338
|
${cmd}
|