jefrichat-mcp 0.48.14 → 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 +54 -5
  2. package/dist/index.js +475 -154
  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",
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(
@@ -41675,11 +41724,11 @@ ${cmd}
41675
41724
  kind: external_exports.enum(["file", "contract", "skill", "note"]).optional()
41676
41725
  }
41677
41726
  },
41678
- async ({ departmentId, path: path4, kind }) => withClient(async (c2) => {
41679
- const fname = np4.basename(path4.startsWith("~") ? path4.slice(1) : path4);
41727
+ async ({ departmentId, path: path5, kind }) => withClient(async (c2) => {
41728
+ const fname = np4.basename(path5.startsWith("~") ? path5.slice(1) : path5);
41680
41729
  const mime = mimeOf(fname);
41681
41730
  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)}`;
41731
+ 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
41732
  return ok3(`To add ${fname} to the department, run in the terminal:
41684
41733
 
41685
41734
  ${cmd}
@@ -41761,10 +41810,10 @@ ${cmd}
41761
41810
  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
41811
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
41763
41812
  },
41764
- async ({ path: path4 }) => withClient(async (c2) => {
41765
- const fname = np4.basename(path4.startsWith("~") ? path4.slice(1) : path4);
41813
+ async ({ path: path5 }) => withClient(async (c2) => {
41814
+ const fname = np4.basename(path5.startsWith("~") ? path5.slice(1) : path5);
41766
41815
  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)}`;
41816
+ 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
41817
  return ok3(
41769
41818
  `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
41819
 
@@ -41942,15 +41991,15 @@ Or just upload ${fname} from the agent's page in the web app.`
41942
41991
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41943
41992
  }
41944
41993
  },
41945
- async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41994
+ async ({ groupId, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41946
41995
  let name, mime, url;
41947
- if (path4) {
41996
+ if (path5) {
41948
41997
  if (!ctx.local) {
41949
- const r = await remoteUploadCommand(c2, ctx, { groupId }, path4, caption, "the group");
41998
+ const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group");
41950
41999
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
41951
42000
  }
41952
- const abs = resolveLocalFile(path4);
41953
- if (!abs) return fail(`I can't find a file at ${path4} on this machine.`);
42001
+ const abs = resolveLocalFile(path5);
42002
+ if (!abs) return fail(`I can't find a file at ${path5} on this machine.`);
41954
42003
  const buf = fs5.readFileSync(abs);
41955
42004
  name = np4.basename(abs);
41956
42005
  mime = mimeOf(name);
@@ -42883,10 +42932,147 @@ async function runDoctor() {
42883
42932
  process.exitCode = problems ? 1 : 0;
42884
42933
  }
42885
42934
 
42935
+ // src/identity.ts
42936
+ import fs11 from "node:fs";
42937
+ import os7 from "node:os";
42938
+ import path3 from "node:path";
42939
+ var IDENTITIES_FILE = path3.join(os7.homedir(), ".jefri", "identities.json");
42940
+ function loadIdentitiesFile(filePath = IDENTITIES_FILE) {
42941
+ let raw;
42942
+ let mode = null;
42943
+ try {
42944
+ const st = fs11.statSync(filePath);
42945
+ mode = st.mode & 511;
42946
+ raw = fs11.readFileSync(filePath, "utf8");
42947
+ } catch (e) {
42948
+ if (e?.code === "ENOENT") return null;
42949
+ const err = new Error(`cannot read ${filePath}: ${e?.message ?? e}`);
42950
+ err.code = "JEFRI_IDENTITIES_UNREADABLE";
42951
+ throw err;
42952
+ }
42953
+ if (process.platform !== "win32" && mode !== null && (mode & 63) !== 0) {
42954
+ const err = new Error(
42955
+ `${filePath} is readable by others (mode ${mode.toString(8)}) \u2014 it holds agent tokens. Run: chmod 600 ${filePath}`
42956
+ );
42957
+ err.code = "JEFRI_IDENTITIES_PERM";
42958
+ throw err;
42959
+ }
42960
+ const parsed = JSON.parse(raw);
42961
+ if (!parsed || typeof parsed !== "object") return null;
42962
+ return parsed;
42963
+ }
42964
+ var expandHome = (p) => p === "~" ? os7.homedir() : p.startsWith("~/") || p.startsWith("~\\") ? path3.join(os7.homedir(), p.slice(2)) : p;
42965
+ var nativeRealpath = (p) => (fs11.realpathSync.native ?? fs11.realpathSync)(p);
42966
+ function canonical(p, realpath) {
42967
+ let r = path3.resolve(expandHome(p));
42968
+ try {
42969
+ r = realpath(r);
42970
+ } catch {
42971
+ }
42972
+ return r;
42973
+ }
42974
+ function matchDirRule(byDir, cwd, realpath = nativeRealpath) {
42975
+ if (!byDir) return null;
42976
+ const c2 = canonical(cwd, realpath);
42977
+ let best = null;
42978
+ for (const [rulePath, value] of Object.entries(byDir)) {
42979
+ const r = canonical(rulePath, realpath);
42980
+ const isMatch = c2 === r || c2.startsWith(r) && (r.endsWith(path3.sep) || c2[r.length] === path3.sep);
42981
+ if (isMatch && (!best || r.length > best.len)) best = { len: r.length, value };
42982
+ }
42983
+ return best?.value ?? null;
42984
+ }
42985
+ function selectableNames(file) {
42986
+ return Object.entries(file?.identities ?? {}).filter(([, p]) => p?.modelSelectable === true).map(([name]) => name).sort();
42987
+ }
42988
+ var POOL_AUTO = "pool:auto";
42989
+ function resolveIdentity(opts = {}) {
42990
+ const env = opts.env ?? process.env;
42991
+ if (env.JEFRI_TOKEN) return { kind: "legacy-token", token: env.JEFRI_TOKEN };
42992
+ if (env.JEFRI_AS) return { kind: "legacy-as", username: env.JEFRI_AS };
42993
+ const file = opts.file ?? null;
42994
+ const identities = file?.identities ?? {};
42995
+ const selectable = () => selectableNames(file);
42996
+ const toProfile = (name, source) => {
42997
+ if (name === POOL_AUTO)
42998
+ return {
42999
+ kind: "error",
43000
+ message: "this rule resolves to pool:auto, but pool assignment is not available yet (SESSIONS-PLAN Phase 3) \u2014 name a specific identity instead"
43001
+ };
43002
+ const p = identities[name];
43003
+ if (!p?.token)
43004
+ return {
43005
+ kind: "error",
43006
+ message: `identity "${name}" (from the ${source} rule) is not defined in ~/.jefri/identities.json \u2014 add it, or fix the rule`
43007
+ };
43008
+ return { kind: "profile", name, token: p.token, source };
43009
+ };
43010
+ if (env.JEFRI_PROFILE) return toProfile(env.JEFRI_PROFILE, "env");
43011
+ const rules = file?.rules ?? {};
43012
+ const dirHit = matchDirRule(rules.byDir, opts.cwd ?? process.cwd(), opts.realpath);
43013
+ if (dirHit) return toProfile(dirHit, "dir");
43014
+ if (opts.clientName && rules.byClient) {
43015
+ const wanted = opts.clientName.trim().toLowerCase();
43016
+ for (const [client, name] of Object.entries(rules.byClient)) {
43017
+ if (client.trim().toLowerCase() === wanted) return toProfile(name, "client");
43018
+ }
43019
+ }
43020
+ if (rules.default) return toProfile(rules.default, "default");
43021
+ return { kind: "unassigned", selectable: selectable() };
43022
+ }
43023
+ function profileForBe(file, name) {
43024
+ const p = file?.identities?.[name];
43025
+ if (!p?.token)
43026
+ return { ok: false, message: `no identity named "${name}" in ~/.jefri/identities.json` };
43027
+ if (p.modelSelectable !== true)
43028
+ return {
43029
+ ok: false,
43030
+ message: `identity "${name}" is not marked modelSelectable \u2014 a human must set "modelSelectable": true in ~/.jefri/identities.json before the model may choose it`
43031
+ };
43032
+ return { ok: true, name, token: p.token };
43033
+ }
43034
+
43035
+ // src/bind.ts
43036
+ function createBinder() {
43037
+ let state = { phase: "unassigned" };
43038
+ let inflight = null;
43039
+ return {
43040
+ state: () => state,
43041
+ current: () => inflight,
43042
+ bind(profile, connect) {
43043
+ if (state.phase === "bound") {
43044
+ return state.profile === profile ? inflight : Promise.reject(
43045
+ new Error(`already bound as "${state.profile}" \u2014 one identity per session; restart the session to re-identify`)
43046
+ );
43047
+ }
43048
+ if (state.phase === "binding") {
43049
+ return state.profile === profile ? inflight : Promise.reject(
43050
+ new Error(`a bind to "${state.profile}" is already in flight \u2014 await it (it may still fail and free the session)`)
43051
+ );
43052
+ }
43053
+ state = { phase: "binding", profile };
43054
+ const p = connect().then(
43055
+ (c2) => {
43056
+ state = { phase: "bound", profile };
43057
+ return c2;
43058
+ },
43059
+ (e) => {
43060
+ state = { phase: "unassigned", lastError: String(e?.message ?? e) };
43061
+ if (inflight === p) inflight = null;
43062
+ throw e;
43063
+ }
43064
+ );
43065
+ inflight = p;
43066
+ return p;
43067
+ }
43068
+ };
43069
+ }
43070
+
42886
43071
  // src/index.ts
42887
43072
  init_version();
42888
43073
  var SERVER = process.env.JEFRI_SERVER ?? "http://localhost:4000";
42889
43074
  var TOKEN = process.env.JEFRI_TOKEN;
43075
+ var LEGACY_MODE = !!(process.env.JEFRI_TOKEN || process.env.JEFRI_AS);
42890
43076
  var USERNAME = process.env.JEFRI_AS ?? "claude_agent";
42891
43077
  var NAME = process.env.JEFRI_NAME ?? USERNAME;
42892
43078
  var TAGS = (process.env.JEFRI_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -42896,15 +43082,15 @@ var VERBOSE = process.env.JEFRI_DEBUG === "1" || process.env.JEFRI_VERBOSE === "
42896
43082
  var note = (...a) => {
42897
43083
  if (VERBOSE) log(...a);
42898
43084
  };
42899
- var TOKEN_CACHE = path3.join(os7.homedir(), ".jefri", "tokens.json");
42900
- var LEGACY_TOKEN_CACHE = path3.join(os7.homedir(), ".acp", "tokens.json");
43085
+ var TOKEN_CACHE = path4.join(os8.homedir(), ".jefri", "tokens.json");
43086
+ var LEGACY_TOKEN_CACHE = path4.join(os8.homedir(), ".acp", "tokens.json");
42901
43087
  var cacheKey = `${SERVER}::${USERNAME}`;
42902
43088
  function readTokenCache() {
42903
43089
  try {
42904
- return JSON.parse(fs11.readFileSync(TOKEN_CACHE, "utf8"));
43090
+ return JSON.parse(fs12.readFileSync(TOKEN_CACHE, "utf8"));
42905
43091
  } catch {
42906
43092
  try {
42907
- return JSON.parse(fs11.readFileSync(LEGACY_TOKEN_CACHE, "utf8"));
43093
+ return JSON.parse(fs12.readFileSync(LEGACY_TOKEN_CACHE, "utf8"));
42908
43094
  } catch {
42909
43095
  return {};
42910
43096
  }
@@ -42913,22 +43099,44 @@ function readTokenCache() {
42913
43099
  function cacheToken(tok) {
42914
43100
  const c2 = readTokenCache();
42915
43101
  c2[cacheKey] = tok;
42916
- fs11.mkdirSync(path3.dirname(TOKEN_CACHE), { recursive: true });
42917
- fs11.writeFileSync(TOKEN_CACHE, JSON.stringify(c2, null, 2));
43102
+ fs12.mkdirSync(path4.dirname(TOKEN_CACHE), { recursive: true });
43103
+ fs12.writeFileSync(TOKEN_CACHE, JSON.stringify(c2, null, 2));
42918
43104
  }
42919
43105
  var clientPromise = null;
42920
43106
  var inbox = [];
42921
43107
  var control = null;
42922
43108
  var detectedHost = null;
43109
+ var binder = createBinder();
43110
+ var resolution = null;
43111
+ var resolvedProfileToken = null;
43112
+ var knownSelectable = [];
43113
+ var unassignedGuidance = () => {
43114
+ const sel = knownSelectable.length ? `Model-selectable identities: ${knownSelectable.join(", ")} \u2014 call jefri_be("<name>") to become one.` : `No model-selectable identities are configured yet.`;
43115
+ const st = binder.state();
43116
+ const failed = st.phase === "unassigned" && st.lastError ? `
43117
+ Last bind attempt failed: ${st.lastError}` : "";
43118
+ return `This session has NO Jefri identity assigned. Three ways to get one:
43119
+ 1. ${sel}
43120
+ 2. Launch with JEFRI_PROFILE=<name> (or JEFRI_AS=<agent> / JEFRI_TOKEN=\u2026).
43121
+ 3. Add a byDir/byClient/default rule to ~/.jefri/identities.json.` + failed;
43122
+ };
43123
+ var ensureClientGated = () => {
43124
+ if (LEGACY_MODE) return ensureClient();
43125
+ const cur = binder.current();
43126
+ if (cur) return cur;
43127
+ if (resolution?.kind === "error") return Promise.reject(new Error(resolution.message));
43128
+ return Promise.reject(new Error(unassignedGuidance()));
43129
+ };
42923
43130
  function ensureClient() {
42924
43131
  if (!clientPromise) {
42925
43132
  clientPromise = (async () => {
42926
- const known = TOKEN ?? readTokenCache()[cacheKey];
43133
+ const known = LEGACY_MODE ? TOKEN ?? readTokenCache()[cacheKey] : resolvedProfileToken;
43134
+ if (!LEGACY_MODE && !known) throw new Error(unassignedGuidance());
42927
43135
  const clientInfo = {
42928
43136
  name: "jefrichat-mcp",
42929
43137
  version: connectorVersion(),
42930
43138
  cwd: process.cwd(),
42931
- machine: os7.hostname()
43139
+ machine: os8.hostname()
42932
43140
  };
42933
43141
  const provision = () => JefriClient.connect({
42934
43142
  server: SERVER,
@@ -42946,10 +43154,19 @@ function ensureClient() {
42946
43154
  c2 = await JefriClient.connect({ server: SERVER, token: known, status: "online", clientInfo });
42947
43155
  if (!c2.identity) {
42948
43156
  if (TOKEN) throw new Error("JEFRI_TOKEN was rejected by the server");
43157
+ if (!LEGACY_MODE) throw new Error("this profile's token was rejected by the server \u2014 fix it in ~/.jefri/identities.json");
42949
43158
  log("cached token rejected, re-provisioning by username");
42950
43159
  c2 = await provision();
42951
43160
  if (c2.identity) cacheToken(c2.token);
42952
43161
  }
43162
+ if (!LEGACY_MODE && c2.identity && c2.identity.type !== "agent") {
43163
+ try {
43164
+ c2.close?.();
43165
+ c2.disconnect?.();
43166
+ } catch {
43167
+ }
43168
+ 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`);
43169
+ }
42953
43170
  } else {
42954
43171
  c2 = await provision();
42955
43172
  if (c2.identity) cacheToken(c2.token);
@@ -42991,78 +43208,6 @@ function ensureClient() {
42991
43208
  }, 3e3);
42992
43209
  });
42993
43210
  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
43211
  const handleIncoming = (m) => {
43067
43212
  const preview = m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : m.content ?? "";
43068
43213
  const isGroup = !!m.groupId;
@@ -43157,6 +43302,78 @@ function ensureClient() {
43157
43302
  );
43158
43303
  });
43159
43304
  c2.on("debate_cancel", (ev) => cancelDebate(String(ev?.debateId ?? ""), Number(ev?.turnSeq ?? -999)));
43305
+ const sendAs = (t, text5) => {
43306
+ if (t.groupId) c2.groupMessage(t.groupId, text5);
43307
+ else if (t.to) c2.message(t.to, text5);
43308
+ };
43309
+ try {
43310
+ control = await startControlServer({
43311
+ self,
43312
+ displayName: c2.identity?.displayName ?? self,
43313
+ owner: c2.identity?.owner,
43314
+ server: SERVER,
43315
+ workdir: getAuto().workdir,
43316
+ cwd: process.cwd(),
43317
+ machine: os8.hostname(),
43318
+ getHost: () => detectedHost,
43319
+ isOnline: () => c2.online,
43320
+ isAutonomous: () => getAuto().enabled,
43321
+ getBrain: () => {
43322
+ const b = brainStatus();
43323
+ return { label: b.resolved, ready: b.ready };
43324
+ },
43325
+ sendText: sendAs,
43326
+ // Read a conversation from the HUB, over the socket this process
43327
+ // already holds. The panel's own inbox only has what arrived while
43328
+ // this connector was running, so a freshly-started UI shows nothing
43329
+ // for a conversation that has been going for weeks — which reads as
43330
+ // broken rather than as empty.
43331
+ fetchHistory: async ({ with: other, groupId, limit }) => {
43332
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
43333
+ const p = waitFor(c2, "history", (e) => e.conversationId === convId);
43334
+ c2.history(convId);
43335
+ const res = await p;
43336
+ const raw = (res?.messages ?? []).slice(-limit);
43337
+ return raw.map((m) => ({
43338
+ id: String(m.id ?? ""),
43339
+ // Raw username for routing, sanitized copy for display — the same
43340
+ // split the panel makes, for the same reason: a name can carry a
43341
+ // bidi override that would reorder the line around it.
43342
+ from: String(m.senderUsername ?? ""),
43343
+ fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
43344
+ text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
43345
+ at: String(m.createdAt ?? ""),
43346
+ mine: String(m.senderUsername ?? "") === self,
43347
+ kind: m.kind === "file" ? "file" : "text",
43348
+ fileName: m.fileName ?? null
43349
+ }));
43350
+ },
43351
+ handleWithBrain: async (m) => {
43352
+ const text5 = await runOnceForMessage({
43353
+ content: m.text,
43354
+ sender: m.from,
43355
+ isGroup: !!m.groupId,
43356
+ groupId: m.groupId,
43357
+ conversationId: m.conversationId,
43358
+ isMention: m.isMention,
43359
+ senderIsBot: agentUsers.has(m.from)
43360
+ });
43361
+ sendAs(m.groupId ? { groupId: m.groupId } : { to: m.from }, text5);
43362
+ return { ok: true, reply: text5 };
43363
+ },
43364
+ log
43365
+ });
43366
+ const popupCommand = `${selfCommand()} popup --agent ${shQuote(self)}`;
43367
+ setClickCommand(popupCommand);
43368
+ const app = installMacApp(`${selfCommand()} popup`, connectorVersion());
43369
+ if (app) log(`Jefri Chat.app ready at ${app} \u2014 drag it to your Dock to read and reply`);
43370
+ } catch (e) {
43371
+ log("control socket unavailable (panel disabled):", e?.message ?? e);
43372
+ }
43373
+ if (macNeedsTerminalNotifier)
43374
+ note("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
43375
+ if (getAuto().enabled)
43376
+ note(`autonomous mode is ON \u2014 incoming messages will be handled by "${getAuto().brain}" in ${getAuto().workdir}`);
43160
43377
  note(`connected to ${SERVER} as ${self}`);
43161
43378
  return c2;
43162
43379
  })().catch((e) => {
@@ -43167,7 +43384,75 @@ function ensureClient() {
43167
43384
  return clientPromise;
43168
43385
  }
43169
43386
  var server = new McpServer({ name: "jefrichat", version: "0.1.0" });
43170
- registerAcpTools(server, { ensureClient, inbox, serverUrl: SERVER, local: true });
43387
+ registerAcpTools(server, {
43388
+ ensureClient: ensureClientGated,
43389
+ inbox,
43390
+ serverUrl: SERVER,
43391
+ local: true,
43392
+ // jefri_whoami in an UNBOUND new-mode session answers from here — listing
43393
+ // ONLY the model-selectable aliases (never tokens, paths, or rule details).
43394
+ identityStatus: async () => {
43395
+ if (LEGACY_MODE) return null;
43396
+ const inflight = binder.current();
43397
+ if (inflight) {
43398
+ try {
43399
+ await inflight;
43400
+ } catch {
43401
+ }
43402
+ }
43403
+ if (binder.state().phase === "bound") return null;
43404
+ if (resolution?.kind === "error") return `Identity configuration problem: ${resolution.message}`;
43405
+ return unassignedGuidance();
43406
+ }
43407
+ });
43408
+ if (!LEGACY_MODE) {
43409
+ server.registerTool(
43410
+ "jefri_be",
43411
+ {
43412
+ title: "Choose this session's Jefri identity",
43413
+ 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.",
43414
+ inputSchema: { name: external_exports.string().describe("the identity's name, as listed by jefri_whoami") }
43415
+ },
43416
+ async ({ name }) => {
43417
+ const already = binder.state();
43418
+ if (already.phase === "bound")
43419
+ return { content: [{ type: "text", text: `Already bound as "${already.profile}" \u2014 one identity per session. Restart the session to re-identify.` }] };
43420
+ const inflight = binder.current();
43421
+ if (inflight) {
43422
+ try {
43423
+ await inflight;
43424
+ } catch {
43425
+ }
43426
+ const after = binder.state();
43427
+ if (after.phase === "bound")
43428
+ return { content: [{ type: "text", text: `Already bound as "${after.profile}" (the automatic rule won) \u2014 restart the session to re-identify.` }] };
43429
+ }
43430
+ let file = null;
43431
+ try {
43432
+ file = loadIdentitiesFile();
43433
+ } catch (e) {
43434
+ return { content: [{ type: "text", text: `Cannot read ~/.jefri/identities.json: ${e?.message ?? e}` }], isError: true };
43435
+ }
43436
+ knownSelectable = selectableNames(file);
43437
+ const fenced = profileForBe(file, name);
43438
+ if (!fenced.ok) return { content: [{ type: "text", text: fenced.message }], isError: true };
43439
+ try {
43440
+ const c2 = await binder.bind(fenced.name, () => {
43441
+ resolvedProfileToken = fenced.token;
43442
+ return ensureClient();
43443
+ });
43444
+ try {
43445
+ c2.announceHost(detectedHost ?? void 0);
43446
+ } catch {
43447
+ }
43448
+ return { content: [{ type: "text", text: `\u2705 You are now @${c2.identity.username} ("${c2.identity.displayName}") for the rest of this session.` }] };
43449
+ } catch (e) {
43450
+ return { content: [{ type: "text", text: `Could not bind "${name}": ${e?.message ?? e}
43451
+ The session is still unassigned \u2014 fix the cause and try again.` }], isError: true };
43452
+ }
43453
+ }
43454
+ );
43455
+ }
43171
43456
  async function main() {
43172
43457
  if (process.argv[2] === "doctor") {
43173
43458
  await runDoctor();
@@ -43197,20 +43482,56 @@ async function main() {
43197
43482
  setTimeout(() => bootConnect(attempt + 1), jittered).unref?.();
43198
43483
  });
43199
43484
  };
43200
- bootConnect();
43485
+ if (LEGACY_MODE) bootConnect();
43201
43486
  const transport = new StdioServerTransport();
43202
43487
  server.server.oninitialized = () => {
43203
43488
  try {
43204
43489
  const rawHost = server.server.getClientVersion()?.name;
43205
43490
  detectedHost = rawHost ?? null;
43206
43491
  setAutoHost(rawHost);
43207
- void ensureClient().then((cl) => cl.announceHost(rawHost)).catch(() => {
43208
- });
43492
+ if (LEGACY_MODE) {
43493
+ void ensureClient().then((cl) => cl.announceHost(rawHost)).catch(() => {
43494
+ });
43495
+ return;
43496
+ }
43497
+ let file = null;
43498
+ try {
43499
+ file = loadIdentitiesFile();
43500
+ } catch (e) {
43501
+ resolution = { kind: "error", message: String(e?.message ?? e) };
43502
+ log(resolution.message);
43503
+ return;
43504
+ }
43505
+ knownSelectable = selectableNames(file);
43506
+ resolution = resolveIdentity({ clientName: rawHost ?? null, file });
43507
+ if (resolution.kind === "error") {
43508
+ log(resolution.message);
43509
+ return;
43510
+ }
43511
+ if (resolution.kind !== "profile") {
43512
+ note("no identity assigned for this session (unassigned mode) \u2014 jefri_whoami explains the options");
43513
+ return;
43514
+ }
43515
+ const prof = resolution;
43516
+ const autoBind = (attempt = 0) => {
43517
+ binder.bind(prof.name, () => {
43518
+ resolvedProfileToken = prof.token;
43519
+ return ensureClient();
43520
+ }).then((cl) => cl.announceHost(rawHost)).catch((e) => {
43521
+ const delay = Math.min(2e3 * 2 ** attempt, 3e4);
43522
+ const jittered = Math.floor(delay / 2 + Math.random() * (delay / 2));
43523
+ log(`bind to "${prof.name}" failed (retrying in ${Math.round(jittered / 1e3)}s):`, e?.message ?? e);
43524
+ setTimeout(() => {
43525
+ if (binder.state().phase === "unassigned") autoBind(attempt + 1);
43526
+ }, jittered).unref?.();
43527
+ });
43528
+ };
43529
+ autoBind();
43209
43530
  } catch {
43210
43531
  }
43211
43532
  };
43212
43533
  await server.connect(transport);
43213
- note(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
43534
+ 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
43535
  let shuttingDown = false;
43215
43536
  const shutdownOnHostClose = () => {
43216
43537
  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.0",
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": {