jefrichat-mcp 0.48.14 → 0.49.1

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 +68 -6
  2. package/dist/index.js +489 -155
  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");
@@ -62535,9 +62580,13 @@ ${e?.message ?? e}`);
62535
62580
  description: "Show this session's Jefri Chat identity (username, display name) and connection status.",
62536
62581
  inputSchema: {}
62537
62582
  },
62538
- async () => withClient(
62539
- async (c) => ok3(`You are "${c.identity.displayName}" (@${c.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
62540
- )
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
+ }
62541
62590
  );
62542
62591
  server2.registerTool(
62543
62592
  "jefri_agents",
@@ -63119,10 +63168,23 @@ ${fp}`);
63119
63168
  }
63120
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}`)}`;
63121
63170
  const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
63171
+ let directLink = "";
63172
+ try {
63173
+ const lr = await fetch(`${hub}/api/files/${match.id}/link`, {
63174
+ method: "POST",
63175
+ headers: { authorization: `Bearer ${c.token}` }
63176
+ });
63177
+ if (lr.ok) {
63178
+ const body = await lr.json();
63179
+ if (body?.url) directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${body.url}
63180
+ `;
63181
+ }
63182
+ } catch {
63183
+ }
63122
63184
  return ok3(
63123
63185
  `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
63124
63186
 
63125
- \u2022 Easiest: open the chat in the web app, the file renders there: ${web2}
63187
+ ` + directLink + `\u2022 Web app (the file renders in the chat): ${web2}
63126
63188
  \u2022 Terminal: ${cmd}
63127
63189
  (set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`
63128
63190
  );
package/dist/index.js CHANGED
@@ -3244,8 +3244,8 @@ var require_utils = __commonJS({
3244
3244
  }
3245
3245
  return ind;
3246
3246
  }
3247
- function removeDotSegments(path4) {
3248
- let input2 = path4;
3247
+ function removeDotSegments(path5) {
3248
+ let input2 = path5;
3249
3249
  const output2 = [];
3250
3250
  let nextSlash = -1;
3251
3251
  let len = 0;
@@ -3497,8 +3497,8 @@ var require_schemes = __commonJS({
3497
3497
  wsComponent.secure = void 0;
3498
3498
  }
3499
3499
  if (wsComponent.resourceName) {
3500
- const [path4, query] = wsComponent.resourceName.split("?");
3501
- wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
3500
+ const [path5, query] = wsComponent.resourceName.split("?");
3501
+ wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3502
3502
  wsComponent.query = query;
3503
3503
  wsComponent.resourceName = void 0;
3504
3504
  }
@@ -6891,12 +6891,12 @@ var require_dist = __commonJS({
6891
6891
  throw new Error(`Unknown format "${name}"`);
6892
6892
  return f;
6893
6893
  };
6894
- function addFormats(ajv, list3, fs12, exportName) {
6894
+ function addFormats(ajv, list3, fs13, exportName) {
6895
6895
  var _a;
6896
6896
  var _b;
6897
6897
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6898
6898
  for (const f of list3)
6899
- ajv.addFormat(f, fs12[f]);
6899
+ ajv.addFormat(f, fs13[f]);
6900
6900
  }
6901
6901
  module.exports = exports = formatsPlugin;
6902
6902
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -10956,10 +10956,10 @@ function ensureDirs() {
10956
10956
  }
10957
10957
  }
10958
10958
  function publishEndpoint(d, deps = {}) {
10959
- const writeFile = deps.writeFileSync ?? ((path4, data, opts) => fs6.writeFileSync(path4, data, opts));
10959
+ const writeFile = deps.writeFileSync ?? ((path5, data, opts) => fs6.writeFileSync(path5, data, opts));
10960
10960
  const rename = deps.renameSync ?? ((from, to) => fs6.renameSync(from, to));
10961
- const unlink = deps.unlinkSync ?? ((path4) => fs6.unlinkSync(path4));
10962
- const readFile = deps.readFileSync ?? ((path4) => fs6.readFileSync(path4, "utf8"));
10961
+ const unlink = deps.unlinkSync ?? ((path5) => fs6.unlinkSync(path5));
10962
+ const readFile = deps.readFileSync ?? ((path5) => fs6.readFileSync(path5, "utf8"));
10963
10963
  ensureDirs();
10964
10964
  if (!d.procToken) d = { ...d, procToken: procToken(d.pid) };
10965
10965
  if (!d.nonce) d = { ...d, nonce: INSTANCE_NONCE };
@@ -13136,8 +13136,8 @@ function getErrorMap() {
13136
13136
 
13137
13137
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
13138
13138
  var makeIssue = (params) => {
13139
- const { data, path: path4, errorMaps, issueData } = params;
13140
- const fullPath = [...path4, ...issueData.path || []];
13139
+ const { data, path: path5, errorMaps, issueData } = params;
13140
+ const fullPath = [...path5, ...issueData.path || []];
13141
13141
  const fullIssue = {
13142
13142
  ...issueData,
13143
13143
  path: fullPath
@@ -13253,11 +13253,11 @@ var errorUtil;
13253
13253
 
13254
13254
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
13255
13255
  var ParseInputLazyPath = class {
13256
- constructor(parent, value, path4, key) {
13256
+ constructor(parent, value, path5, key) {
13257
13257
  this._cachedPath = [];
13258
13258
  this.parent = parent;
13259
13259
  this.data = value;
13260
- this._path = path4;
13260
+ this._path = path5;
13261
13261
  this._key = key;
13262
13262
  }
13263
13263
  get path() {
@@ -16894,10 +16894,10 @@ function assignProp(target, prop, value) {
16894
16894
  configurable: true
16895
16895
  });
16896
16896
  }
16897
- function getElementAtPath(obj, path4) {
16898
- if (!path4)
16897
+ function getElementAtPath(obj, path5) {
16898
+ if (!path5)
16899
16899
  return obj;
16900
- return path4.reduce((acc, key) => acc?.[key], obj);
16900
+ return path5.reduce((acc, key) => acc?.[key], obj);
16901
16901
  }
16902
16902
  function promiseAllObject(promisesObj) {
16903
16903
  const keys = Object.keys(promisesObj);
@@ -17217,11 +17217,11 @@ function aborted(x, startIndex = 0) {
17217
17217
  }
17218
17218
  return false;
17219
17219
  }
17220
- function prefixIssues(path4, issues) {
17220
+ function prefixIssues(path5, issues) {
17221
17221
  return issues.map((iss) => {
17222
17222
  var _a;
17223
17223
  (_a = iss).path ?? (_a.path = []);
17224
- iss.path.unshift(path4);
17224
+ iss.path.unshift(path5);
17225
17225
  return iss;
17226
17226
  });
17227
17227
  }
@@ -26871,9 +26871,9 @@ var StdioServerTransport = class {
26871
26871
  };
26872
26872
 
26873
26873
  // src/index.ts
26874
- import fs11 from "node:fs";
26875
- import os7 from "node:os";
26876
- import path3 from "node:path";
26874
+ import fs12 from "node:fs";
26875
+ import os8 from "node:os";
26876
+ import path4 from "node:path";
26877
26877
 
26878
26878
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
26879
26879
  var import_stream = __toESM(require_stream(), 1);
@@ -27350,13 +27350,13 @@ var VFile = class {
27350
27350
  * @returns {undefined}
27351
27351
  * Nothing.
27352
27352
  */
27353
- set path(path4) {
27354
- if (isUrl(path4)) {
27355
- path4 = fileURLToPath(path4);
27353
+ set path(path5) {
27354
+ if (isUrl(path5)) {
27355
+ path5 = fileURLToPath(path5);
27356
27356
  }
27357
- assertNonEmpty(path4, "path");
27358
- if (this.path !== path4) {
27359
- this.history.push(path4);
27357
+ assertNonEmpty(path5, "path");
27358
+ if (this.path !== path5) {
27359
+ this.history.push(path5);
27360
27360
  }
27361
27361
  }
27362
27362
  /**
@@ -27623,8 +27623,8 @@ function assertNonEmpty(part, name) {
27623
27623
  throw new Error("`" + name + "` cannot be empty");
27624
27624
  }
27625
27625
  }
27626
- function assertPath(path4, name) {
27627
- if (!path4) {
27626
+ function assertPath(path5, name) {
27627
+ if (!path5) {
27628
27628
  throw new Error("Setting `" + name + "` requires `path` to be set too");
27629
27629
  }
27630
27630
  }
@@ -35610,7 +35610,7 @@ function transformGfmAutolinkLiterals(tree) {
35610
35610
  { ignore: ["link", "linkReference"] }
35611
35611
  );
35612
35612
  }
35613
- function findUrl(_, protocol, domain2, path4, match) {
35613
+ function findUrl(_, protocol, domain2, path5, match) {
35614
35614
  let prefix = "";
35615
35615
  if (!previous2(match)) {
35616
35616
  return false;
@@ -35623,7 +35623,7 @@ function findUrl(_, protocol, domain2, path4, match) {
35623
35623
  if (!isCorrectDomain(domain2)) {
35624
35624
  return false;
35625
35625
  }
35626
- const parts = splitUrl(domain2 + path4);
35626
+ const parts = splitUrl(domain2 + path5);
35627
35627
  if (!parts[0]) return false;
35628
35628
  const result = {
35629
35629
  type: "link",
@@ -38638,6 +38638,22 @@ var JefriAuthError = class extends Error {
38638
38638
  var JefriClient = class _JefriClient {
38639
38639
  ws;
38640
38640
  handlers = /* @__PURE__ */ new Map();
38641
+ // Round 2 of the sessions audit (finding #1): a message can arrive in the
38642
+ // milliseconds between `registered` and the caller attaching its handlers —
38643
+ // connect() itself awaits E2E setup after registration, and the connector
38644
+ // wires its inbox after connect() resolves. Events nobody is listening for
38645
+ // yet are BUFFERED (delivery-critical types only) and replayed, in order,
38646
+ // to the first subscriber. Live listeners bypass the buffer entirely.
38647
+ static REPLAYABLE = /* @__PURE__ */ new Set([
38648
+ "message_received",
38649
+ "file_received",
38650
+ "debate_turn",
38651
+ "debate_summary_request",
38652
+ "debate_cancel"
38653
+ ]);
38654
+ static REPLAY_MAX = 500;
38655
+ replayBuffer = [];
38656
+ replayScheduled = false;
38641
38657
  server;
38642
38658
  token;
38643
38659
  identity;
@@ -38894,14 +38910,33 @@ var JefriClient = class _JefriClient {
38894
38910
  const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
38895
38911
  set.add(handler);
38896
38912
  this.handlers.set(event, set);
38913
+ if (this.replayBuffer.length && _JefriClient.REPLAYABLE.has(event)) this.scheduleReplayDrain();
38897
38914
  return this;
38898
38915
  }
38899
38916
  off(event, handler) {
38900
38917
  this.handlers.get(event)?.delete(handler);
38901
38918
  return this;
38902
38919
  }
38903
- emit(event, payload) {
38904
- for (const h of this.handlers.get(event) ?? []) {
38920
+ scheduleReplayDrain() {
38921
+ if (this.replayScheduled) return;
38922
+ this.replayScheduled = true;
38923
+ queueMicrotask(() => {
38924
+ this.replayScheduled = false;
38925
+ const drain2 = this.replayBuffer;
38926
+ this.replayBuffer = [];
38927
+ const keep = [];
38928
+ for (const b of drain2) {
38929
+ const hs = this.handlers.get(b.event);
38930
+ if (hs?.size) this.dispatch(b.event, b.payload, hs);
38931
+ else keep.push(b);
38932
+ }
38933
+ this.replayBuffer = keep.concat(this.replayBuffer);
38934
+ });
38935
+ }
38936
+ /** emit()'s containment, shared with replay: a throwing handler must NEVER
38937
+ * propagate or starve the others; async rejections are contained too. */
38938
+ dispatch(event, payload, handlers) {
38939
+ for (const h of handlers) {
38905
38940
  try {
38906
38941
  const res = h(payload);
38907
38942
  if (res instanceof Promise)
@@ -38911,6 +38946,16 @@ var JefriClient = class _JefriClient {
38911
38946
  }
38912
38947
  }
38913
38948
  }
38949
+ emit(event, payload) {
38950
+ if (_JefriClient.REPLAYABLE.has(event) && !this.handlers.get(event)?.size) {
38951
+ if (this.replayBuffer.length >= _JefriClient.REPLAY_MAX) {
38952
+ this.replayBuffer.shift();
38953
+ console.error(`[jefri-sdk] replay buffer overflow for pre-subscription events \u2014 oldest dropped`);
38954
+ }
38955
+ this.replayBuffer.push({ event, payload });
38956
+ }
38957
+ this.dispatch(event, payload, this.handlers.get(event) ?? []);
38958
+ }
38914
38959
  send(ev) {
38915
38960
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
38916
38961
  throw new Error("not connected to the Jefri Chat hub (reconnecting) \u2014 try again in a moment");
@@ -40595,8 +40640,8 @@ var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${p
40595
40640
  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);
40596
40641
  var DATAURL_INLINE_MAX = 256 * 1024;
40597
40642
  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;
40598
- async function remoteUploadCommand(c2, ctx, target, path4, caption, where) {
40599
- const fname = crossBasename(path4);
40643
+ async function remoteUploadCommand(c2, ctx, target, path5, caption, where) {
40644
+ const fname = crossBasename(path5);
40600
40645
  const fmime = mimeOf(fname);
40601
40646
  const hub = ctx.serverUrl.replace(/\/$/, "");
40602
40647
  try {
@@ -40611,10 +40656,10 @@ async function remoteUploadCommand(c2, ctx, target, path4, caption, where) {
40611
40656
  return {
40612
40657
  ok: `This cloud connector can't read your disk \u2014 but here is the fast path. Run this ONE command in the terminal (it uploads AND delivers the file to ${where}):
40613
40658
 
40614
- curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}
40659
+ curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path5)}
40615
40660
 
40616
40661
  Windows PowerShell instead:
40617
- Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path4)}
40662
+ Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path5)}
40618
40663
 
40619
40664
  The link is single-use and expires in ${mins} min; the file name and caption are already attached to it. Run it now, then confirm delivery from its JSON response ({"ok":true,...}). Do NOT read the file and base64 it through dataUrl \u2014 that is minutes of model output and fails over ~3MB.`
40620
40665
  };
@@ -40624,7 +40669,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
40624
40669
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
40625
40670
  const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
40626
40671
  const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
40627
- const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}`;
40672
+ const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path5)}`;
40628
40673
  return {
40629
40674
  fail: `This is the cloud connector, so it can't read "${fname}" off your computer, and the upload-link service didn't answer. Options:
40630
40675
 
@@ -40998,9 +41043,13 @@ ${e?.message ?? e}`);
40998
41043
  description: "Show this session's Jefri Chat identity (username, display name) and connection status.",
40999
41044
  inputSchema: {}
41000
41045
  },
41001
- async () => withClient(
41002
- async (c2) => ok3(`You are "${c2.identity.displayName}" (@${c2.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
41003
- )
41046
+ async () => {
41047
+ const unbound = await ctx.identityStatus?.();
41048
+ if (unbound) return ok3(unbound);
41049
+ return withClient(
41050
+ async (c2) => ok3(`You are "${c2.identity.displayName}" (@${c2.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
41051
+ );
41052
+ }
41004
41053
  );
41005
41054
  server2.registerTool(
41006
41055
  "jefri_agents",
@@ -41026,8 +41075,8 @@ ${e?.message ?? e}`);
41026
41075
  })
41027
41076
  );
41028
41077
  const NOT_OWNER = "This only works when you run the Jefri connector as your OWN (human) account and own this agent. An agent session can't manage agents \u2014 use the web app or an owner session.";
41029
- const ownerApi = async (c2, path4, method) => {
41030
- const res = await fetch(`${ctx.serverUrl}${path4}`, {
41078
+ const ownerApi = async (c2, path5, method) => {
41079
+ const res = await fetch(`${ctx.serverUrl}${path5}`, {
41031
41080
  method,
41032
41081
  headers: { Authorization: `Bearer ${c2.token}` }
41033
41082
  });
@@ -41159,18 +41208,18 @@ ${fp}`);
41159
41208
  caption: external_exports.string().optional().describe("optional private E2E caption sent as a separate encrypted message")
41160
41209
  }
41161
41210
  },
41162
- async ({ to, path: path4, dataUrl, fileName, caption }) => withClient(async (c2) => {
41211
+ async ({ to, path: path5, dataUrl, fileName, caption }) => withClient(async (c2) => {
41163
41212
  let name, mime, url;
41164
- if (path4) {
41213
+ if (path5) {
41165
41214
  if (!ctx.local) {
41166
41215
  return fail(
41167
41216
  `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.`
41168
41217
  );
41169
41218
  }
41170
- const abs = resolveLocalFile(path4);
41219
+ const abs = resolveLocalFile(path5);
41171
41220
  if (!abs) {
41172
41221
  return fail(
41173
- `I can't find a file at ${path4} on this machine.`
41222
+ `I can't find a file at ${path5} on this machine.`
41174
41223
  );
41175
41224
  }
41176
41225
  const buf = fs5.readFileSync(abs);
@@ -41222,18 +41271,18 @@ ${fp}`);
41222
41271
  caption: external_exports.string().optional().describe("optional private E2E group caption sent separately")
41223
41272
  }
41224
41273
  },
41225
- async ({ groupId, path: path4, dataUrl, fileName, caption }) => withClient(async (c2) => {
41274
+ async ({ groupId, path: path5, dataUrl, fileName, caption }) => withClient(async (c2) => {
41226
41275
  let name, mime, url;
41227
- if (path4) {
41276
+ if (path5) {
41228
41277
  if (!ctx.local) {
41229
41278
  return fail(
41230
41279
  `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.`
41231
41280
  );
41232
41281
  }
41233
- const abs = resolveLocalFile(path4);
41282
+ const abs = resolveLocalFile(path5);
41234
41283
  if (!abs) {
41235
41284
  return fail(
41236
- `I can't find a file at ${path4} on this machine.`
41285
+ `I can't find a file at ${path5} on this machine.`
41237
41286
  );
41238
41287
  }
41239
41288
  const buf = fs5.readFileSync(abs);
@@ -41266,16 +41315,16 @@ ${fp}`);
41266
41315
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41267
41316
  }
41268
41317
  },
41269
- async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41318
+ async ({ to, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41270
41319
  let name, mime, url;
41271
- if (path4) {
41320
+ if (path5) {
41272
41321
  if (!ctx.local) {
41273
- const r = await remoteUploadCommand(c2, ctx, { to }, path4, caption, `@${to}`);
41322
+ const r = await remoteUploadCommand(c2, ctx, { to }, path5, caption, `@${to}`);
41274
41323
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
41275
41324
  }
41276
- const abs = resolveLocalFile(path4);
41325
+ const abs = resolveLocalFile(path5);
41277
41326
  if (!abs) {
41278
- return fail(`I can't find a file at ${path4} on this machine.`);
41327
+ return fail(`I can't find a file at ${path5} on this machine.`);
41279
41328
  }
41280
41329
  const buf = fs5.readFileSync(abs);
41281
41330
  name = np4.basename(abs);
@@ -41393,12 +41442,12 @@ ${fp}`);
41393
41442
  caption: external_exports.string().optional().describe("optional text caption to send with it")
41394
41443
  }
41395
41444
  },
41396
- async ({ to, path: path4, caption }) => withClient(async (c2) => {
41445
+ async ({ to, path: path5, caption }) => withClient(async (c2) => {
41397
41446
  if (!ctx.local)
41398
41447
  return fail(
41399
41448
  `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.`
41400
41449
  );
41401
- const rawDir = path4 && path4.trim() ? path4 : process.cwd();
41450
+ const rawDir = path5 && path5.trim() ? path5 : process.cwd();
41402
41451
  const dir = rawDir.startsWith("~") ? np4.join(os4.homedir(), rawDir.slice(1)) : rawDir;
41403
41452
  if (!fs5.existsSync(dir))
41404
41453
  return fail(
@@ -41582,10 +41631,23 @@ ${fp}`);
41582
41631
  }
41583
41632
  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}`)}`;
41584
41633
  const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41634
+ let directLink = "";
41635
+ try {
41636
+ const lr = await fetch(`${hub}/api/files/${match.id}/link`, {
41637
+ method: "POST",
41638
+ headers: { authorization: `Bearer ${c2.token}` }
41639
+ });
41640
+ if (lr.ok) {
41641
+ const body = await lr.json();
41642
+ if (body?.url) directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${body.url}
41643
+ `;
41644
+ }
41645
+ } catch {
41646
+ }
41585
41647
  return ok3(
41586
41648
  `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
41587
41649
 
41588
- \u2022 Easiest: open the chat in the web app, the file renders there: ${web2}
41650
+ ` + directLink + `\u2022 Web app (the file renders in the chat): ${web2}
41589
41651
  \u2022 Terminal: ${cmd}
41590
41652
  (set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`
41591
41653
  );
@@ -41675,11 +41737,11 @@ ${cmd}
41675
41737
  kind: external_exports.enum(["file", "contract", "skill", "note"]).optional()
41676
41738
  }
41677
41739
  },
41678
- async ({ departmentId, path: path4, kind }) => withClient(async (c2) => {
41679
- const fname = np4.basename(path4.startsWith("~") ? path4.slice(1) : path4);
41740
+ async ({ departmentId, path: path5, kind }) => withClient(async (c2) => {
41741
+ const fname = np4.basename(path5.startsWith("~") ? path5.slice(1) : path5);
41680
41742
  const mime = mimeOf(fname);
41681
41743
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
41682
- const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/departments/${departmentId}/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${mime}" --data-binary @${shq(path4)}`;
41744
+ const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/departments/${departmentId}/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${mime}" --data-binary @${shq(path5)}`;
41683
41745
  return ok3(`To add ${fname} to the department, run in the terminal:
41684
41746
 
41685
41747
  ${cmd}
@@ -41761,10 +41823,10 @@ ${cmd}
41761
41823
  description: "Add a local file to this agent's document memory. Memory is CURATED BY THE OWNER, so this returns a command that uses the OWNER token (the agent token can't add docs) \u2014 or the owner can upload it from the agent's page in the web app. Documents follow the agent's visibility (public \u2192 owner's connections can read; shared \u2192 chosen people; private \u2192 owner only). Warn before uploading anything sensitive to a public agent.",
41762
41824
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
41763
41825
  },
41764
- async ({ path: path4 }) => withClient(async (c2) => {
41765
- const fname = np4.basename(path4.startsWith("~") ? path4.slice(1) : path4);
41826
+ async ({ path: path5 }) => withClient(async (c2) => {
41827
+ const fname = np4.basename(path5.startsWith("~") ? path5.slice(1) : path5);
41766
41828
  const mime = mimeOf(fname);
41767
- const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c2.identity.username)}/docs?name=${encodeURIComponent(fname)}`)} -H "Authorization: Bearer $JEFRI_OWNER_TOKEN" -H "Content-Type: ${mime}" --data-binary @${shq(path4)}`;
41829
+ const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c2.identity.username)}/docs?name=${encodeURIComponent(fname)}`)} -H "Authorization: Bearer $JEFRI_OWNER_TOKEN" -H "Content-Type: ${mime}" --data-binary @${shq(path5)}`;
41768
41830
  return ok3(
41769
41831
  `Adding to agent memory is OWNER-only, so this uses your owner token. Set it first (export JEFRI_OWNER_TOKEN=\u2026 from the app), then run:
41770
41832
 
@@ -41942,15 +42004,15 @@ Or just upload ${fname} from the agent's page in the web app.`
41942
42004
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41943
42005
  }
41944
42006
  },
41945
- async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
42007
+ async ({ groupId, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41946
42008
  let name, mime, url;
41947
- if (path4) {
42009
+ if (path5) {
41948
42010
  if (!ctx.local) {
41949
- const r = await remoteUploadCommand(c2, ctx, { groupId }, path4, caption, "the group");
42011
+ const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group");
41950
42012
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
41951
42013
  }
41952
- const abs = resolveLocalFile(path4);
41953
- if (!abs) return fail(`I can't find a file at ${path4} on this machine.`);
42014
+ const abs = resolveLocalFile(path5);
42015
+ if (!abs) return fail(`I can't find a file at ${path5} on this machine.`);
41954
42016
  const buf = fs5.readFileSync(abs);
41955
42017
  name = np4.basename(abs);
41956
42018
  mime = mimeOf(name);
@@ -42883,10 +42945,147 @@ async function runDoctor() {
42883
42945
  process.exitCode = problems ? 1 : 0;
42884
42946
  }
42885
42947
 
42948
+ // src/identity.ts
42949
+ import fs11 from "node:fs";
42950
+ import os7 from "node:os";
42951
+ import path3 from "node:path";
42952
+ var IDENTITIES_FILE = path3.join(os7.homedir(), ".jefri", "identities.json");
42953
+ function loadIdentitiesFile(filePath = IDENTITIES_FILE) {
42954
+ let raw;
42955
+ let mode = null;
42956
+ try {
42957
+ const st = fs11.statSync(filePath);
42958
+ mode = st.mode & 511;
42959
+ raw = fs11.readFileSync(filePath, "utf8");
42960
+ } catch (e) {
42961
+ if (e?.code === "ENOENT") return null;
42962
+ const err = new Error(`cannot read ${filePath}: ${e?.message ?? e}`);
42963
+ err.code = "JEFRI_IDENTITIES_UNREADABLE";
42964
+ throw err;
42965
+ }
42966
+ if (process.platform !== "win32" && mode !== null && (mode & 63) !== 0) {
42967
+ const err = new Error(
42968
+ `${filePath} is readable by others (mode ${mode.toString(8)}) \u2014 it holds agent tokens. Run: chmod 600 ${filePath}`
42969
+ );
42970
+ err.code = "JEFRI_IDENTITIES_PERM";
42971
+ throw err;
42972
+ }
42973
+ const parsed = JSON.parse(raw);
42974
+ if (!parsed || typeof parsed !== "object") return null;
42975
+ return parsed;
42976
+ }
42977
+ var expandHome = (p) => p === "~" ? os7.homedir() : p.startsWith("~/") || p.startsWith("~\\") ? path3.join(os7.homedir(), p.slice(2)) : p;
42978
+ var nativeRealpath = (p) => (fs11.realpathSync.native ?? fs11.realpathSync)(p);
42979
+ function canonical(p, realpath) {
42980
+ let r = path3.resolve(expandHome(p));
42981
+ try {
42982
+ r = realpath(r);
42983
+ } catch {
42984
+ }
42985
+ return r;
42986
+ }
42987
+ function matchDirRule(byDir, cwd, realpath = nativeRealpath) {
42988
+ if (!byDir) return null;
42989
+ const c2 = canonical(cwd, realpath);
42990
+ let best = null;
42991
+ for (const [rulePath, value] of Object.entries(byDir)) {
42992
+ const r = canonical(rulePath, realpath);
42993
+ const isMatch = c2 === r || c2.startsWith(r) && (r.endsWith(path3.sep) || c2[r.length] === path3.sep);
42994
+ if (isMatch && (!best || r.length > best.len)) best = { len: r.length, value };
42995
+ }
42996
+ return best?.value ?? null;
42997
+ }
42998
+ function selectableNames(file) {
42999
+ return Object.entries(file?.identities ?? {}).filter(([, p]) => p?.modelSelectable === true).map(([name]) => name).sort();
43000
+ }
43001
+ var POOL_AUTO = "pool:auto";
43002
+ function resolveIdentity(opts = {}) {
43003
+ const env = opts.env ?? process.env;
43004
+ if (env.JEFRI_TOKEN) return { kind: "legacy-token", token: env.JEFRI_TOKEN };
43005
+ if (env.JEFRI_AS) return { kind: "legacy-as", username: env.JEFRI_AS };
43006
+ const file = opts.file ?? null;
43007
+ const identities = file?.identities ?? {};
43008
+ const selectable = () => selectableNames(file);
43009
+ const toProfile = (name, source) => {
43010
+ if (name === POOL_AUTO)
43011
+ return {
43012
+ kind: "error",
43013
+ message: "this rule resolves to pool:auto, but pool assignment is not available yet (SESSIONS-PLAN Phase 3) \u2014 name a specific identity instead"
43014
+ };
43015
+ const p = identities[name];
43016
+ if (!p?.token)
43017
+ return {
43018
+ kind: "error",
43019
+ message: `identity "${name}" (from the ${source} rule) is not defined in ~/.jefri/identities.json \u2014 add it, or fix the rule`
43020
+ };
43021
+ return { kind: "profile", name, token: p.token, source };
43022
+ };
43023
+ if (env.JEFRI_PROFILE) return toProfile(env.JEFRI_PROFILE, "env");
43024
+ const rules = file?.rules ?? {};
43025
+ const dirHit = matchDirRule(rules.byDir, opts.cwd ?? process.cwd(), opts.realpath);
43026
+ if (dirHit) return toProfile(dirHit, "dir");
43027
+ if (opts.clientName && rules.byClient) {
43028
+ const wanted = opts.clientName.trim().toLowerCase();
43029
+ for (const [client, name] of Object.entries(rules.byClient)) {
43030
+ if (client.trim().toLowerCase() === wanted) return toProfile(name, "client");
43031
+ }
43032
+ }
43033
+ if (rules.default) return toProfile(rules.default, "default");
43034
+ return { kind: "unassigned", selectable: selectable() };
43035
+ }
43036
+ function profileForBe(file, name) {
43037
+ const p = file?.identities?.[name];
43038
+ if (!p?.token)
43039
+ return { ok: false, message: `no identity named "${name}" in ~/.jefri/identities.json` };
43040
+ if (p.modelSelectable !== true)
43041
+ return {
43042
+ ok: false,
43043
+ message: `identity "${name}" is not marked modelSelectable \u2014 a human must set "modelSelectable": true in ~/.jefri/identities.json before the model may choose it`
43044
+ };
43045
+ return { ok: true, name, token: p.token };
43046
+ }
43047
+
43048
+ // src/bind.ts
43049
+ function createBinder() {
43050
+ let state = { phase: "unassigned" };
43051
+ let inflight = null;
43052
+ return {
43053
+ state: () => state,
43054
+ current: () => inflight,
43055
+ bind(profile, connect) {
43056
+ if (state.phase === "bound") {
43057
+ return state.profile === profile ? inflight : Promise.reject(
43058
+ new Error(`already bound as "${state.profile}" \u2014 one identity per session; restart the session to re-identify`)
43059
+ );
43060
+ }
43061
+ if (state.phase === "binding") {
43062
+ return state.profile === profile ? inflight : Promise.reject(
43063
+ new Error(`a bind to "${state.profile}" is already in flight \u2014 await it (it may still fail and free the session)`)
43064
+ );
43065
+ }
43066
+ state = { phase: "binding", profile };
43067
+ const p = connect().then(
43068
+ (c2) => {
43069
+ state = { phase: "bound", profile };
43070
+ return c2;
43071
+ },
43072
+ (e) => {
43073
+ state = { phase: "unassigned", lastError: String(e?.message ?? e) };
43074
+ if (inflight === p) inflight = null;
43075
+ throw e;
43076
+ }
43077
+ );
43078
+ inflight = p;
43079
+ return p;
43080
+ }
43081
+ };
43082
+ }
43083
+
42886
43084
  // src/index.ts
42887
43085
  init_version();
42888
43086
  var SERVER = process.env.JEFRI_SERVER ?? "http://localhost:4000";
42889
43087
  var TOKEN = process.env.JEFRI_TOKEN;
43088
+ var LEGACY_MODE = !!(process.env.JEFRI_TOKEN || process.env.JEFRI_AS);
42890
43089
  var USERNAME = process.env.JEFRI_AS ?? "claude_agent";
42891
43090
  var NAME = process.env.JEFRI_NAME ?? USERNAME;
42892
43091
  var TAGS = (process.env.JEFRI_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -42896,15 +43095,15 @@ var VERBOSE = process.env.JEFRI_DEBUG === "1" || process.env.JEFRI_VERBOSE === "
42896
43095
  var note = (...a) => {
42897
43096
  if (VERBOSE) log(...a);
42898
43097
  };
42899
- var TOKEN_CACHE = path3.join(os7.homedir(), ".jefri", "tokens.json");
42900
- var LEGACY_TOKEN_CACHE = path3.join(os7.homedir(), ".acp", "tokens.json");
43098
+ var TOKEN_CACHE = path4.join(os8.homedir(), ".jefri", "tokens.json");
43099
+ var LEGACY_TOKEN_CACHE = path4.join(os8.homedir(), ".acp", "tokens.json");
42901
43100
  var cacheKey = `${SERVER}::${USERNAME}`;
42902
43101
  function readTokenCache() {
42903
43102
  try {
42904
- return JSON.parse(fs11.readFileSync(TOKEN_CACHE, "utf8"));
43103
+ return JSON.parse(fs12.readFileSync(TOKEN_CACHE, "utf8"));
42905
43104
  } catch {
42906
43105
  try {
42907
- return JSON.parse(fs11.readFileSync(LEGACY_TOKEN_CACHE, "utf8"));
43106
+ return JSON.parse(fs12.readFileSync(LEGACY_TOKEN_CACHE, "utf8"));
42908
43107
  } catch {
42909
43108
  return {};
42910
43109
  }
@@ -42913,22 +43112,44 @@ function readTokenCache() {
42913
43112
  function cacheToken(tok) {
42914
43113
  const c2 = readTokenCache();
42915
43114
  c2[cacheKey] = tok;
42916
- fs11.mkdirSync(path3.dirname(TOKEN_CACHE), { recursive: true });
42917
- fs11.writeFileSync(TOKEN_CACHE, JSON.stringify(c2, null, 2));
43115
+ fs12.mkdirSync(path4.dirname(TOKEN_CACHE), { recursive: true });
43116
+ fs12.writeFileSync(TOKEN_CACHE, JSON.stringify(c2, null, 2));
42918
43117
  }
42919
43118
  var clientPromise = null;
42920
43119
  var inbox = [];
42921
43120
  var control = null;
42922
43121
  var detectedHost = null;
43122
+ var binder = createBinder();
43123
+ var resolution = null;
43124
+ var resolvedProfileToken = null;
43125
+ var knownSelectable = [];
43126
+ var unassignedGuidance = () => {
43127
+ const sel = knownSelectable.length ? `Model-selectable identities: ${knownSelectable.join(", ")} \u2014 call jefri_be("<name>") to become one.` : `No model-selectable identities are configured yet.`;
43128
+ const st = binder.state();
43129
+ const failed = st.phase === "unassigned" && st.lastError ? `
43130
+ Last bind attempt failed: ${st.lastError}` : "";
43131
+ return `This session has NO Jefri identity assigned. Three ways to get one:
43132
+ 1. ${sel}
43133
+ 2. Launch with JEFRI_PROFILE=<name> (or JEFRI_AS=<agent> / JEFRI_TOKEN=\u2026).
43134
+ 3. Add a byDir/byClient/default rule to ~/.jefri/identities.json.` + failed;
43135
+ };
43136
+ var ensureClientGated = () => {
43137
+ if (LEGACY_MODE) return ensureClient();
43138
+ const cur = binder.current();
43139
+ if (cur) return cur;
43140
+ if (resolution?.kind === "error") return Promise.reject(new Error(resolution.message));
43141
+ return Promise.reject(new Error(unassignedGuidance()));
43142
+ };
42923
43143
  function ensureClient() {
42924
43144
  if (!clientPromise) {
42925
43145
  clientPromise = (async () => {
42926
- const known = TOKEN ?? readTokenCache()[cacheKey];
43146
+ const known = LEGACY_MODE ? TOKEN ?? readTokenCache()[cacheKey] : resolvedProfileToken;
43147
+ if (!LEGACY_MODE && !known) throw new Error(unassignedGuidance());
42927
43148
  const clientInfo = {
42928
43149
  name: "jefrichat-mcp",
42929
43150
  version: connectorVersion(),
42930
43151
  cwd: process.cwd(),
42931
- machine: os7.hostname()
43152
+ machine: os8.hostname()
42932
43153
  };
42933
43154
  const provision = () => JefriClient.connect({
42934
43155
  server: SERVER,
@@ -42946,10 +43167,19 @@ function ensureClient() {
42946
43167
  c2 = await JefriClient.connect({ server: SERVER, token: known, status: "online", clientInfo });
42947
43168
  if (!c2.identity) {
42948
43169
  if (TOKEN) throw new Error("JEFRI_TOKEN was rejected by the server");
43170
+ if (!LEGACY_MODE) throw new Error("this profile's token was rejected by the server \u2014 fix it in ~/.jefri/identities.json");
42949
43171
  log("cached token rejected, re-provisioning by username");
42950
43172
  c2 = await provision();
42951
43173
  if (c2.identity) cacheToken(c2.token);
42952
43174
  }
43175
+ if (!LEGACY_MODE && c2.identity && c2.identity.type !== "agent") {
43176
+ try {
43177
+ c2.close?.();
43178
+ c2.disconnect?.();
43179
+ } catch {
43180
+ }
43181
+ throw new Error(`the token for this profile resolves to a ${c2.identity.type} identity (@${c2.identity.username}) \u2014 sessions may only bind AGENT identities`);
43182
+ }
42953
43183
  } else {
42954
43184
  c2 = await provision();
42955
43185
  if (c2.identity) cacheToken(c2.token);
@@ -42991,78 +43221,6 @@ function ensureClient() {
42991
43221
  }, 3e3);
42992
43222
  });
42993
43223
  configureAuto({ self, owner: c2.identity?.owner, getHistory });
42994
- const sendAs = (t, text5) => {
42995
- if (t.groupId) c2.groupMessage(t.groupId, text5);
42996
- else if (t.to) c2.message(t.to, text5);
42997
- };
42998
- try {
42999
- control = await startControlServer({
43000
- self,
43001
- displayName: c2.identity?.displayName ?? self,
43002
- owner: c2.identity?.owner,
43003
- server: SERVER,
43004
- workdir: getAuto().workdir,
43005
- cwd: process.cwd(),
43006
- machine: os7.hostname(),
43007
- getHost: () => detectedHost,
43008
- isOnline: () => c2.online,
43009
- isAutonomous: () => getAuto().enabled,
43010
- getBrain: () => {
43011
- const b = brainStatus();
43012
- return { label: b.resolved, ready: b.ready };
43013
- },
43014
- sendText: sendAs,
43015
- // Read a conversation from the HUB, over the socket this process
43016
- // already holds. The panel's own inbox only has what arrived while
43017
- // this connector was running, so a freshly-started UI shows nothing
43018
- // for a conversation that has been going for weeks — which reads as
43019
- // broken rather than as empty.
43020
- fetchHistory: async ({ with: other, groupId, limit }) => {
43021
- const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
43022
- const p = waitFor(c2, "history", (e) => e.conversationId === convId);
43023
- c2.history(convId);
43024
- const res = await p;
43025
- const raw = (res?.messages ?? []).slice(-limit);
43026
- return raw.map((m) => ({
43027
- id: String(m.id ?? ""),
43028
- // Raw username for routing, sanitized copy for display — the same
43029
- // split the panel makes, for the same reason: a name can carry a
43030
- // bidi override that would reorder the line around it.
43031
- from: String(m.senderUsername ?? ""),
43032
- fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
43033
- text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
43034
- at: String(m.createdAt ?? ""),
43035
- mine: String(m.senderUsername ?? "") === self,
43036
- kind: m.kind === "file" ? "file" : "text",
43037
- fileName: m.fileName ?? null
43038
- }));
43039
- },
43040
- handleWithBrain: async (m) => {
43041
- const text5 = await runOnceForMessage({
43042
- content: m.text,
43043
- sender: m.from,
43044
- isGroup: !!m.groupId,
43045
- groupId: m.groupId,
43046
- conversationId: m.conversationId,
43047
- isMention: m.isMention,
43048
- senderIsBot: agentUsers.has(m.from)
43049
- });
43050
- sendAs(m.groupId ? { groupId: m.groupId } : { to: m.from }, text5);
43051
- return { ok: true, reply: text5 };
43052
- },
43053
- log
43054
- });
43055
- const popupCommand = `${selfCommand()} popup --agent ${shQuote(self)}`;
43056
- setClickCommand(popupCommand);
43057
- const app = installMacApp(`${selfCommand()} popup`, connectorVersion());
43058
- if (app) log(`Jefri Chat.app ready at ${app} \u2014 drag it to your Dock to read and reply`);
43059
- } catch (e) {
43060
- log("control socket unavailable (panel disabled):", e?.message ?? e);
43061
- }
43062
- if (macNeedsTerminalNotifier)
43063
- note("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
43064
- if (getAuto().enabled)
43065
- note(`autonomous mode is ON \u2014 incoming messages will be handled by "${getAuto().brain}" in ${getAuto().workdir}`);
43066
43224
  const handleIncoming = (m) => {
43067
43225
  const preview = m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : m.content ?? "";
43068
43226
  const isGroup = !!m.groupId;
@@ -43157,6 +43315,78 @@ function ensureClient() {
43157
43315
  );
43158
43316
  });
43159
43317
  c2.on("debate_cancel", (ev) => cancelDebate(String(ev?.debateId ?? ""), Number(ev?.turnSeq ?? -999)));
43318
+ const sendAs = (t, text5) => {
43319
+ if (t.groupId) c2.groupMessage(t.groupId, text5);
43320
+ else if (t.to) c2.message(t.to, text5);
43321
+ };
43322
+ try {
43323
+ control = await startControlServer({
43324
+ self,
43325
+ displayName: c2.identity?.displayName ?? self,
43326
+ owner: c2.identity?.owner,
43327
+ server: SERVER,
43328
+ workdir: getAuto().workdir,
43329
+ cwd: process.cwd(),
43330
+ machine: os8.hostname(),
43331
+ getHost: () => detectedHost,
43332
+ isOnline: () => c2.online,
43333
+ isAutonomous: () => getAuto().enabled,
43334
+ getBrain: () => {
43335
+ const b = brainStatus();
43336
+ return { label: b.resolved, ready: b.ready };
43337
+ },
43338
+ sendText: sendAs,
43339
+ // Read a conversation from the HUB, over the socket this process
43340
+ // already holds. The panel's own inbox only has what arrived while
43341
+ // this connector was running, so a freshly-started UI shows nothing
43342
+ // for a conversation that has been going for weeks — which reads as
43343
+ // broken rather than as empty.
43344
+ fetchHistory: async ({ with: other, groupId, limit }) => {
43345
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
43346
+ const p = waitFor(c2, "history", (e) => e.conversationId === convId);
43347
+ c2.history(convId);
43348
+ const res = await p;
43349
+ const raw = (res?.messages ?? []).slice(-limit);
43350
+ return raw.map((m) => ({
43351
+ id: String(m.id ?? ""),
43352
+ // Raw username for routing, sanitized copy for display — the same
43353
+ // split the panel makes, for the same reason: a name can carry a
43354
+ // bidi override that would reorder the line around it.
43355
+ from: String(m.senderUsername ?? ""),
43356
+ fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
43357
+ text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
43358
+ at: String(m.createdAt ?? ""),
43359
+ mine: String(m.senderUsername ?? "") === self,
43360
+ kind: m.kind === "file" ? "file" : "text",
43361
+ fileName: m.fileName ?? null
43362
+ }));
43363
+ },
43364
+ handleWithBrain: async (m) => {
43365
+ const text5 = await runOnceForMessage({
43366
+ content: m.text,
43367
+ sender: m.from,
43368
+ isGroup: !!m.groupId,
43369
+ groupId: m.groupId,
43370
+ conversationId: m.conversationId,
43371
+ isMention: m.isMention,
43372
+ senderIsBot: agentUsers.has(m.from)
43373
+ });
43374
+ sendAs(m.groupId ? { groupId: m.groupId } : { to: m.from }, text5);
43375
+ return { ok: true, reply: text5 };
43376
+ },
43377
+ log
43378
+ });
43379
+ const popupCommand = `${selfCommand()} popup --agent ${shQuote(self)}`;
43380
+ setClickCommand(popupCommand);
43381
+ const app = installMacApp(`${selfCommand()} popup`, connectorVersion());
43382
+ if (app) log(`Jefri Chat.app ready at ${app} \u2014 drag it to your Dock to read and reply`);
43383
+ } catch (e) {
43384
+ log("control socket unavailable (panel disabled):", e?.message ?? e);
43385
+ }
43386
+ if (macNeedsTerminalNotifier)
43387
+ note("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
43388
+ if (getAuto().enabled)
43389
+ note(`autonomous mode is ON \u2014 incoming messages will be handled by "${getAuto().brain}" in ${getAuto().workdir}`);
43160
43390
  note(`connected to ${SERVER} as ${self}`);
43161
43391
  return c2;
43162
43392
  })().catch((e) => {
@@ -43167,7 +43397,75 @@ function ensureClient() {
43167
43397
  return clientPromise;
43168
43398
  }
43169
43399
  var server = new McpServer({ name: "jefrichat", version: "0.1.0" });
43170
- registerAcpTools(server, { ensureClient, inbox, serverUrl: SERVER, local: true });
43400
+ registerAcpTools(server, {
43401
+ ensureClient: ensureClientGated,
43402
+ inbox,
43403
+ serverUrl: SERVER,
43404
+ local: true,
43405
+ // jefri_whoami in an UNBOUND new-mode session answers from here — listing
43406
+ // ONLY the model-selectable aliases (never tokens, paths, or rule details).
43407
+ identityStatus: async () => {
43408
+ if (LEGACY_MODE) return null;
43409
+ const inflight = binder.current();
43410
+ if (inflight) {
43411
+ try {
43412
+ await inflight;
43413
+ } catch {
43414
+ }
43415
+ }
43416
+ if (binder.state().phase === "bound") return null;
43417
+ if (resolution?.kind === "error") return `Identity configuration problem: ${resolution.message}`;
43418
+ return unassignedGuidance();
43419
+ }
43420
+ });
43421
+ if (!LEGACY_MODE) {
43422
+ server.registerTool(
43423
+ "jefri_be",
43424
+ {
43425
+ title: "Choose this session's Jefri identity",
43426
+ description: "Bind this UNASSIGNED session to one of the model-selectable identities from ~/.jefri/identities.json. Works once per session (one agent = one session); a session that is already someone must be restarted to re-identify.",
43427
+ inputSchema: { name: external_exports.string().describe("the identity's name, as listed by jefri_whoami") }
43428
+ },
43429
+ async ({ name }) => {
43430
+ const already = binder.state();
43431
+ if (already.phase === "bound")
43432
+ return { content: [{ type: "text", text: `Already bound as "${already.profile}" \u2014 one identity per session. Restart the session to re-identify.` }] };
43433
+ const inflight = binder.current();
43434
+ if (inflight) {
43435
+ try {
43436
+ await inflight;
43437
+ } catch {
43438
+ }
43439
+ const after = binder.state();
43440
+ if (after.phase === "bound")
43441
+ return { content: [{ type: "text", text: `Already bound as "${after.profile}" (the automatic rule won) \u2014 restart the session to re-identify.` }] };
43442
+ }
43443
+ let file = null;
43444
+ try {
43445
+ file = loadIdentitiesFile();
43446
+ } catch (e) {
43447
+ return { content: [{ type: "text", text: `Cannot read ~/.jefri/identities.json: ${e?.message ?? e}` }], isError: true };
43448
+ }
43449
+ knownSelectable = selectableNames(file);
43450
+ const fenced = profileForBe(file, name);
43451
+ if (!fenced.ok) return { content: [{ type: "text", text: fenced.message }], isError: true };
43452
+ try {
43453
+ const c2 = await binder.bind(fenced.name, () => {
43454
+ resolvedProfileToken = fenced.token;
43455
+ return ensureClient();
43456
+ });
43457
+ try {
43458
+ c2.announceHost(detectedHost ?? void 0);
43459
+ } catch {
43460
+ }
43461
+ return { content: [{ type: "text", text: `\u2705 You are now @${c2.identity.username} ("${c2.identity.displayName}") for the rest of this session.` }] };
43462
+ } catch (e) {
43463
+ return { content: [{ type: "text", text: `Could not bind "${name}": ${e?.message ?? e}
43464
+ The session is still unassigned \u2014 fix the cause and try again.` }], isError: true };
43465
+ }
43466
+ }
43467
+ );
43468
+ }
43171
43469
  async function main() {
43172
43470
  if (process.argv[2] === "doctor") {
43173
43471
  await runDoctor();
@@ -43197,20 +43495,56 @@ async function main() {
43197
43495
  setTimeout(() => bootConnect(attempt + 1), jittered).unref?.();
43198
43496
  });
43199
43497
  };
43200
- bootConnect();
43498
+ if (LEGACY_MODE) bootConnect();
43201
43499
  const transport = new StdioServerTransport();
43202
43500
  server.server.oninitialized = () => {
43203
43501
  try {
43204
43502
  const rawHost = server.server.getClientVersion()?.name;
43205
43503
  detectedHost = rawHost ?? null;
43206
43504
  setAutoHost(rawHost);
43207
- void ensureClient().then((cl) => cl.announceHost(rawHost)).catch(() => {
43208
- });
43505
+ if (LEGACY_MODE) {
43506
+ void ensureClient().then((cl) => cl.announceHost(rawHost)).catch(() => {
43507
+ });
43508
+ return;
43509
+ }
43510
+ let file = null;
43511
+ try {
43512
+ file = loadIdentitiesFile();
43513
+ } catch (e) {
43514
+ resolution = { kind: "error", message: String(e?.message ?? e) };
43515
+ log(resolution.message);
43516
+ return;
43517
+ }
43518
+ knownSelectable = selectableNames(file);
43519
+ resolution = resolveIdentity({ clientName: rawHost ?? null, file });
43520
+ if (resolution.kind === "error") {
43521
+ log(resolution.message);
43522
+ return;
43523
+ }
43524
+ if (resolution.kind !== "profile") {
43525
+ note("no identity assigned for this session (unassigned mode) \u2014 jefri_whoami explains the options");
43526
+ return;
43527
+ }
43528
+ const prof = resolution;
43529
+ const autoBind = (attempt = 0) => {
43530
+ binder.bind(prof.name, () => {
43531
+ resolvedProfileToken = prof.token;
43532
+ return ensureClient();
43533
+ }).then((cl) => cl.announceHost(rawHost)).catch((e) => {
43534
+ const delay = Math.min(2e3 * 2 ** attempt, 3e4);
43535
+ const jittered = Math.floor(delay / 2 + Math.random() * (delay / 2));
43536
+ log(`bind to "${prof.name}" failed (retrying in ${Math.round(jittered / 1e3)}s):`, e?.message ?? e);
43537
+ setTimeout(() => {
43538
+ if (binder.state().phase === "unassigned") autoBind(attempt + 1);
43539
+ }, jittered).unref?.();
43540
+ });
43541
+ };
43542
+ autoBind();
43209
43543
  } catch {
43210
43544
  }
43211
43545
  };
43212
43546
  await server.connect(transport);
43213
- note(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
43547
+ note(LEGACY_MODE ? `MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}` : `MCP server ready \u2014 identity resolves after the MCP handshake (Jefri Chat server ${SERVER})`);
43214
43548
  let shuttingDown = false;
43215
43549
  const shutdownOnHostClose = () => {
43216
43550
  if (shuttingDown) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.14",
3
+ "version": "0.49.1",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {