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.
Files changed (3) hide show
  1. package/dist/http.js +153 -22
  2. package/dist/index.js +574 -171
  3. 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
- emit(event, payload) {
61170
- for (const h of this.handlers.get(event) ?? []) {
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 () => withClient(
62523
- async (c) => ok3(`You are "${c.identity.displayName}" (@${c.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
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: "Download / save a file someone sent you",
63008
- 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.",
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("where to save it (default: the file's own name in the current folder)")
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 out = savePath || match.fileName || "downloaded.file";
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
- fs4.writeFileSync(outPath, buf);
63034
- return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
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 cmd = `curl -s -o ${shq(out)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
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
- `To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
63042
-
63043
- ${cmd}
63172
+ `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
63044
63173
 
63045
- It downloads the file to your machine (current folder unless you set a path). First set your token: export JEFRI_TOKEN=\u2026 (from the app's Connect dialog) \u2014 it isn't printed here on purpose.`
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("where to save (default: the file's name in the current folder)")
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 ${shq(name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
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 ${shq(d.name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
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}