jefrichat-mcp 0.48.6 → 0.48.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +22 -0
- package/dist/index.js +379 -45
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -60976,6 +60976,28 @@ var JefriClient = class _JefriClient {
|
|
|
60976
60976
|
announceHost(host) {
|
|
60977
60977
|
const h = (host ?? "").trim();
|
|
60978
60978
|
this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
|
|
60979
|
+
this.sendClientInfo();
|
|
60980
|
+
}
|
|
60981
|
+
/** Announce WHERE this connector runs — the folder and the machine.
|
|
60982
|
+
*
|
|
60983
|
+
* Separate from announceHost because the two are learned at different times
|
|
60984
|
+
* and by different means: the host app arrives from the MCP handshake, the
|
|
60985
|
+
* location is simply observed. Callers pass values they READ NOW
|
|
60986
|
+
* (process.cwd(), os.hostname()) — never a configured preference, which is
|
|
60987
|
+
* what made the old reported folder wrong. Omitted fields are left untouched
|
|
60988
|
+
* by the hub rather than cleared, so a caller that knows only one may send
|
|
60989
|
+
* only that one. */
|
|
60990
|
+
announceWhere(where) {
|
|
60991
|
+
const base = this.clientInfo ?? { name: "jefri-sdk", version: "0" };
|
|
60992
|
+
const next = { ...base };
|
|
60993
|
+
if (where.cwd !== void 0) next.cwd = (where.cwd ?? "").trim();
|
|
60994
|
+
if (where.machine !== void 0) next.machine = (where.machine ?? "").trim();
|
|
60995
|
+
this.clientInfo = next;
|
|
60996
|
+
this.sendClientInfo();
|
|
60997
|
+
}
|
|
60998
|
+
/** Push the current clientInfo if the socket is live. Reconnects re-send it
|
|
60999
|
+
* from the stored copy, so a failure here is not worth reporting. */
|
|
61000
|
+
sendClientInfo() {
|
|
60979
61001
|
try {
|
|
60980
61002
|
if (this.ws?.readyState === this.ws?.OPEN)
|
|
60981
61003
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
package/dist/index.js
CHANGED
|
@@ -10847,8 +10847,10 @@ var init_control_proto = __esm({
|
|
|
10847
10847
|
// {agent?, ids?} → {unread: UnreadTally}
|
|
10848
10848
|
"unread",
|
|
10849
10849
|
// {} → UnreadTally
|
|
10850
|
-
"subscribe"
|
|
10850
|
+
"subscribe",
|
|
10851
10851
|
// {} → {subscribed: true}; server then pushes EventFrames
|
|
10852
|
+
"history"
|
|
10853
|
+
// {agent, with?|groupId?, limit?} → {messages: HistoryMessage[]}
|
|
10852
10854
|
];
|
|
10853
10855
|
METHOD_SET = new Set(METHODS);
|
|
10854
10856
|
FrameDecoder = class {
|
|
@@ -11441,6 +11443,17 @@ async function openSession() {
|
|
|
11441
11443
|
return {
|
|
11442
11444
|
conns,
|
|
11443
11445
|
agents: () => conns.flatMap((c2) => c2.agents),
|
|
11446
|
+
async history(agent, q) {
|
|
11447
|
+
const conn = conns.find((c2) => c2.agents.some((a) => a.username === agent)) ?? conns[0];
|
|
11448
|
+
if (!conn) return [];
|
|
11449
|
+
const res = await conn.request("history", {
|
|
11450
|
+
agent,
|
|
11451
|
+
with: q.with,
|
|
11452
|
+
groupId: q.groupId,
|
|
11453
|
+
limit: q.limit ?? 50
|
|
11454
|
+
});
|
|
11455
|
+
return res.messages ?? [];
|
|
11456
|
+
},
|
|
11444
11457
|
async inbox(limit = 50) {
|
|
11445
11458
|
const per = await Promise.allSettled(
|
|
11446
11459
|
conns.map((c2) => c2.request("inbox", { limit }))
|
|
@@ -12104,9 +12117,9 @@ __export(run_exports, {
|
|
|
12104
12117
|
tmuxPanelSetupArgv: () => tmuxPanelSetupArgv,
|
|
12105
12118
|
tmuxRunArgv: () => tmuxRunArgv
|
|
12106
12119
|
});
|
|
12107
|
-
import
|
|
12120
|
+
import cp9 from "node:child_process";
|
|
12108
12121
|
import { createHash as createHash2 } from "node:crypto";
|
|
12109
|
-
import
|
|
12122
|
+
import readline2 from "node:readline";
|
|
12110
12123
|
function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
|
|
12111
12124
|
const env = envPairs.flatMap((e) => ["-e", e]);
|
|
12112
12125
|
return { cmd: "tmux", args: ["new-session", "-d", "-s", sessionName, "-c", cwd, ...env, "--", ...agentCmd] };
|
|
@@ -12157,7 +12170,7 @@ function tmuxPanelSetupArgv(session, launcher, badgeScript) {
|
|
|
12157
12170
|
}
|
|
12158
12171
|
function hasTmux() {
|
|
12159
12172
|
try {
|
|
12160
|
-
|
|
12173
|
+
cp9.execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
12161
12174
|
return true;
|
|
12162
12175
|
} catch {
|
|
12163
12176
|
return false;
|
|
@@ -12175,9 +12188,9 @@ function resolveTmuxInstall(platform, has = hasCmd2) {
|
|
|
12175
12188
|
const command = mgr === "pacman" ? "sudo pacman -S tmux" : mgr === "apk" ? "sudo apk add tmux" : mgr ? `sudo ${mgr} install tmux` : "install tmux with your distro's package manager";
|
|
12176
12189
|
return { kind: "linux-manual", command };
|
|
12177
12190
|
}
|
|
12178
|
-
async function
|
|
12191
|
+
async function promptYesNoDefaultYes2(question) {
|
|
12179
12192
|
if (!process.stdin.isTTY) return false;
|
|
12180
|
-
const rl =
|
|
12193
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
12181
12194
|
try {
|
|
12182
12195
|
const ans = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
|
|
12183
12196
|
return ans === "" || ans === "y" || ans === "yes";
|
|
@@ -12196,7 +12209,7 @@ async function ensureTmuxInteractive() {
|
|
|
12196
12209
|
`
|
|
12197
12210
|
);
|
|
12198
12211
|
if (plan.kind === "brew") {
|
|
12199
|
-
const yes = await
|
|
12212
|
+
const yes = await promptYesNoDefaultYes2(` Install tmux now with Homebrew? (no password / no sudo) [Y/n] `);
|
|
12200
12213
|
if (!yes) {
|
|
12201
12214
|
process.stderr.write(` No problem \u2014 install it any time with: ${plan.command}
|
|
12202
12215
|
`);
|
|
@@ -12205,7 +12218,7 @@ async function ensureTmuxInteractive() {
|
|
|
12205
12218
|
process.stderr.write(` Installing tmux via Homebrew\u2026
|
|
12206
12219
|
`);
|
|
12207
12220
|
try {
|
|
12208
|
-
|
|
12221
|
+
cp9.execFileSync("brew", ["install", "tmux"], { stdio: "inherit" });
|
|
12209
12222
|
} catch {
|
|
12210
12223
|
process.stderr.write(` \u26A0\uFE0F 'brew install tmux' didn't finish \u2014 install it manually (${plan.command}), then re-run this command.
|
|
12211
12224
|
`);
|
|
@@ -12236,7 +12249,7 @@ async function runWrapped(argv) {
|
|
|
12236
12249
|
return 2;
|
|
12237
12250
|
}
|
|
12238
12251
|
const spawnInherit = (cmd2, args2) => new Promise((res) => {
|
|
12239
|
-
const child =
|
|
12252
|
+
const child = cp9.spawn(cmd2, args2, { stdio: "inherit" });
|
|
12240
12253
|
child.on("error", (e) => {
|
|
12241
12254
|
process.stderr.write(`jefri run: could not start ${cmd2}: ${e?.message ?? e}
|
|
12242
12255
|
`);
|
|
@@ -12287,21 +12300,21 @@ async function runWrapped(argv) {
|
|
|
12287
12300
|
const cwd = process.cwd();
|
|
12288
12301
|
const name = sessionNameFor(agentCmd, cwd, sessionOptedIn);
|
|
12289
12302
|
const { cmd, args } = tmuxRunArgv(agentCmd, name, cwd, sessionOptedIn ? ["JEFRI_EXPERIMENTAL_SESSION=1"] : []);
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12303
|
+
cp9.spawnSync(cmd, args, { stdio: "ignore" });
|
|
12304
|
+
cp9.spawnSync("tmux", ["set-option", "-t", name, "mouse", "on"], { stdio: "ignore" });
|
|
12305
|
+
cp9.spawnSync("tmux", ["set-option", "-t", name, "set-clipboard", "on"], { stdio: "ignore" });
|
|
12306
|
+
cp9.spawnSync("tmux", ["set-option", "-t", name, "history-limit", "50000"], { stdio: "ignore" });
|
|
12294
12307
|
for (const table of ["copy-mode", "copy-mode-vi"]) {
|
|
12295
|
-
|
|
12296
|
-
|
|
12308
|
+
cp9.spawnSync("tmux", ["bind-key", "-T", table, "WheelUpPane", "send-keys", "-N2", "-X", "scroll-up"], { stdio: "ignore" });
|
|
12309
|
+
cp9.spawnSync("tmux", ["bind-key", "-T", table, "WheelDownPane", "send-keys", "-N2", "-X", "scroll-down"], { stdio: "ignore" });
|
|
12297
12310
|
if (process.platform === "darwin")
|
|
12298
|
-
|
|
12311
|
+
cp9.spawnSync("tmux", ["bind-key", "-T", table, "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", "pbcopy"], { stdio: "ignore" });
|
|
12299
12312
|
}
|
|
12300
12313
|
try {
|
|
12301
12314
|
const launcher = writePanelLauncher(`${selfCommand()} panel`);
|
|
12302
12315
|
const badge = writeBadgeScript(UNREAD_DIR, AGENTS_DIR);
|
|
12303
12316
|
for (const { cmd: c2, args: a2 } of tmuxPanelSetupArgv(name, launcher, badge))
|
|
12304
|
-
|
|
12317
|
+
cp9.spawnSync(c2, a2, { stdio: "ignore" });
|
|
12305
12318
|
process.stderr.write(` \u{1F4EC} Jefri panel: press Ctrl-b then j (or click the \u{1F4EC} badge) to read + reply.
|
|
12306
12319
|
|
|
12307
12320
|
`);
|
|
@@ -12333,7 +12346,7 @@ __export(panel_exports, {
|
|
|
12333
12346
|
runPanel: () => runPanel,
|
|
12334
12347
|
truncateToWidth: () => truncateToWidth
|
|
12335
12348
|
});
|
|
12336
|
-
import
|
|
12349
|
+
import readline3 from "node:readline/promises";
|
|
12337
12350
|
import { stdin as input, stdout as output } from "node:process";
|
|
12338
12351
|
function ago(at, now = Date.now()) {
|
|
12339
12352
|
const s = Math.max(0, Math.round((now - at) / 1e3));
|
|
@@ -12342,15 +12355,15 @@ function ago(at, now = Date.now()) {
|
|
|
12342
12355
|
if (s < 86400) return `${Math.round(s / 3600)}h`;
|
|
12343
12356
|
return `${Math.round(s / 86400)}d`;
|
|
12344
12357
|
}
|
|
12345
|
-
function charWidth(
|
|
12346
|
-
if (
|
|
12347
|
-
const wide =
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12358
|
+
function charWidth(cp10) {
|
|
12359
|
+
if (cp10 >= 768 && cp10 <= 879 || cp10 >= 6832 && cp10 <= 6911 || cp10 >= 7616 && cp10 <= 7679 || cp10 >= 8400 && cp10 <= 8432 || cp10 >= 65056 && cp10 <= 65071 || cp10 >= 8203 && cp10 <= 8205 || cp10 >= 65024 && cp10 <= 65039 || cp10 >= 917536 && cp10 <= 917631 || cp10 >= 917760 && cp10 <= 917999) return 0;
|
|
12360
|
+
const wide = cp10 >= 4352 && cp10 <= 4447 || // Hangul Jamo
|
|
12361
|
+
cp10 >= 11904 && cp10 <= 42191 && cp10 !== 12351 || // CJK radicals … Yi
|
|
12362
|
+
cp10 >= 44032 && cp10 <= 55203 || // Hangul syllables
|
|
12363
|
+
cp10 >= 63744 && cp10 <= 64255 || // CJK compatibility ideographs
|
|
12364
|
+
cp10 >= 65040 && cp10 <= 65049 || cp10 >= 65072 && cp10 <= 65135 || cp10 >= 65280 && cp10 <= 65376 || cp10 >= 65504 && cp10 <= 65510 || cp10 >= 127462 && cp10 <= 127487 || // regional indicators (flags)
|
|
12365
|
+
cp10 >= 127744 && cp10 <= 129791 || // every modern emoji block
|
|
12366
|
+
cp10 >= 131072 && cp10 <= 262141;
|
|
12354
12367
|
return wide ? 2 : 1;
|
|
12355
12368
|
}
|
|
12356
12369
|
function clusters(s) {
|
|
@@ -12476,7 +12489,7 @@ async function runPanel() {
|
|
|
12476
12489
|
dirty = true;
|
|
12477
12490
|
});
|
|
12478
12491
|
await session.subscribe();
|
|
12479
|
-
const rl =
|
|
12492
|
+
const rl = readline3.createInterface({ input, output });
|
|
12480
12493
|
const prompter = new Prompter(rl);
|
|
12481
12494
|
let messages = [];
|
|
12482
12495
|
const refresh = async () => {
|
|
@@ -38929,6 +38942,28 @@ var JefriClient = class _JefriClient {
|
|
|
38929
38942
|
announceHost(host) {
|
|
38930
38943
|
const h = (host ?? "").trim();
|
|
38931
38944
|
this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
|
|
38945
|
+
this.sendClientInfo();
|
|
38946
|
+
}
|
|
38947
|
+
/** Announce WHERE this connector runs — the folder and the machine.
|
|
38948
|
+
*
|
|
38949
|
+
* Separate from announceHost because the two are learned at different times
|
|
38950
|
+
* and by different means: the host app arrives from the MCP handshake, the
|
|
38951
|
+
* location is simply observed. Callers pass values they READ NOW
|
|
38952
|
+
* (process.cwd(), os.hostname()) — never a configured preference, which is
|
|
38953
|
+
* what made the old reported folder wrong. Omitted fields are left untouched
|
|
38954
|
+
* by the hub rather than cleared, so a caller that knows only one may send
|
|
38955
|
+
* only that one. */
|
|
38956
|
+
announceWhere(where) {
|
|
38957
|
+
const base = this.clientInfo ?? { name: "jefri-sdk", version: "0" };
|
|
38958
|
+
const next = { ...base };
|
|
38959
|
+
if (where.cwd !== void 0) next.cwd = (where.cwd ?? "").trim();
|
|
38960
|
+
if (where.machine !== void 0) next.machine = (where.machine ?? "").trim();
|
|
38961
|
+
this.clientInfo = next;
|
|
38962
|
+
this.sendClientInfo();
|
|
38963
|
+
}
|
|
38964
|
+
/** Push the current clientInfo if the socket is live. Reconnects re-send it
|
|
38965
|
+
* from the stored copy, so a failure here is not worth reporting. */
|
|
38966
|
+
sendClientInfo() {
|
|
38932
38967
|
try {
|
|
38933
38968
|
if (this.ws?.readyState === this.ws?.OPEN)
|
|
38934
38969
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
@@ -39590,31 +39625,90 @@ function configureAuto(opts) {
|
|
|
39590
39625
|
}
|
|
39591
39626
|
var watcher = null;
|
|
39592
39627
|
var watchDebounce = null;
|
|
39628
|
+
var watchRetry = null;
|
|
39629
|
+
var pollTimer = null;
|
|
39630
|
+
var pollSig = "";
|
|
39631
|
+
var watchAttempt = 0;
|
|
39632
|
+
var POLL_MS = 3e3;
|
|
39633
|
+
function applyConfigChange() {
|
|
39634
|
+
const wasEnabled = cfg.enabled;
|
|
39635
|
+
cfg = loadCfg();
|
|
39636
|
+
if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
|
|
39637
|
+
if (cfg.enabled) acquireResponderLock();
|
|
39638
|
+
else {
|
|
39639
|
+
releaseResponderLock();
|
|
39640
|
+
stopAllWork();
|
|
39641
|
+
}
|
|
39642
|
+
ensureIntervalTimer();
|
|
39643
|
+
}
|
|
39644
|
+
function configPath() {
|
|
39645
|
+
return np3.join(AUTO_DIR, `${safeUser(selfName)}.json`);
|
|
39646
|
+
}
|
|
39647
|
+
function configSignature() {
|
|
39648
|
+
try {
|
|
39649
|
+
const st = fs4.statSync(configPath());
|
|
39650
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
39651
|
+
} catch {
|
|
39652
|
+
return "";
|
|
39653
|
+
}
|
|
39654
|
+
}
|
|
39655
|
+
function startPolling(why) {
|
|
39656
|
+
if (pollTimer) return;
|
|
39657
|
+
pollSig = configSignature();
|
|
39658
|
+
logLine(`config hot-reload switched to polling every ${POLL_MS / 1e3}s (${why}) \u2014 no descriptors needed`);
|
|
39659
|
+
pollTimer = setInterval(() => {
|
|
39660
|
+
const sig = configSignature();
|
|
39661
|
+
if (sig === pollSig) return;
|
|
39662
|
+
pollSig = sig;
|
|
39663
|
+
applyConfigChange();
|
|
39664
|
+
}, POLL_MS);
|
|
39665
|
+
pollTimer.unref?.();
|
|
39666
|
+
}
|
|
39667
|
+
function stopPolling() {
|
|
39668
|
+
if (!pollTimer) return;
|
|
39669
|
+
clearInterval(pollTimer);
|
|
39670
|
+
pollTimer = null;
|
|
39671
|
+
}
|
|
39593
39672
|
function watchConfig() {
|
|
39594
39673
|
try {
|
|
39595
39674
|
watcher?.close();
|
|
39596
39675
|
} catch {
|
|
39597
39676
|
}
|
|
39677
|
+
watcher = null;
|
|
39678
|
+
if (watchRetry) {
|
|
39679
|
+
clearTimeout(watchRetry);
|
|
39680
|
+
watchRetry = null;
|
|
39681
|
+
}
|
|
39682
|
+
const degrade = (why) => {
|
|
39683
|
+
startPolling(why);
|
|
39684
|
+
const delay = Math.min(3e4 * 2 ** watchAttempt++, 3e5);
|
|
39685
|
+
watchRetry = setTimeout(watchConfig, delay);
|
|
39686
|
+
watchRetry.unref?.();
|
|
39687
|
+
};
|
|
39598
39688
|
try {
|
|
39599
39689
|
fs4.mkdirSync(AUTO_DIR, { recursive: true });
|
|
39600
39690
|
const file = `${safeUser(selfName)}.json`;
|
|
39601
|
-
|
|
39691
|
+
const w = fs4.watch(AUTO_DIR, { persistent: false }, (_evt, name) => {
|
|
39602
39692
|
if (name && name !== file) return;
|
|
39603
39693
|
if (watchDebounce) clearTimeout(watchDebounce);
|
|
39604
|
-
watchDebounce = setTimeout(
|
|
39605
|
-
const wasEnabled = cfg.enabled;
|
|
39606
|
-
cfg = loadCfg();
|
|
39607
|
-
if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
|
|
39608
|
-
if (cfg.enabled) acquireResponderLock();
|
|
39609
|
-
else {
|
|
39610
|
-
releaseResponderLock();
|
|
39611
|
-
stopAllWork();
|
|
39612
|
-
}
|
|
39613
|
-
ensureIntervalTimer();
|
|
39614
|
-
}, 200);
|
|
39694
|
+
watchDebounce = setTimeout(applyConfigChange, 200);
|
|
39615
39695
|
});
|
|
39616
|
-
|
|
39617
|
-
|
|
39696
|
+
w.on("error", (err) => {
|
|
39697
|
+
logLine(`config watch failed (${err?.code ?? err?.message ?? err}) \u2014 falling back to polling, connector unaffected`);
|
|
39698
|
+
try {
|
|
39699
|
+
w.close();
|
|
39700
|
+
} catch {
|
|
39701
|
+
}
|
|
39702
|
+
if (watcher === w) watcher = null;
|
|
39703
|
+
degrade(String(err?.code ?? "watch error"));
|
|
39704
|
+
});
|
|
39705
|
+
watcher = w;
|
|
39706
|
+
watchAttempt = 0;
|
|
39707
|
+
stopPolling();
|
|
39708
|
+
w.unref?.();
|
|
39709
|
+
} catch (e) {
|
|
39710
|
+
logLine(`config watch unavailable (${e?.code ?? e?.message ?? e}) \u2014 using polling instead`);
|
|
39711
|
+
degrade(String(e?.code ?? "watch unavailable"));
|
|
39618
39712
|
}
|
|
39619
39713
|
}
|
|
39620
39714
|
var LOCK_STALE_MS = 9e4;
|
|
@@ -41856,6 +41950,9 @@ function attachInbox(c2, inbox2, self, onIncoming) {
|
|
|
41856
41950
|
c2.on("file_received", capture);
|
|
41857
41951
|
}
|
|
41858
41952
|
|
|
41953
|
+
// src/index.ts
|
|
41954
|
+
init_control_proto();
|
|
41955
|
+
|
|
41859
41956
|
// src/control.ts
|
|
41860
41957
|
init_control_proto();
|
|
41861
41958
|
init_registry();
|
|
@@ -41883,6 +41980,8 @@ async function startControlServer(opts) {
|
|
|
41883
41980
|
owner: opts.owner ?? null,
|
|
41884
41981
|
host: opts.getHost(),
|
|
41885
41982
|
workdir: opts.workdir,
|
|
41983
|
+
cwd: opts.cwd ?? null,
|
|
41984
|
+
machine: opts.machine ?? null,
|
|
41886
41985
|
server: opts.server,
|
|
41887
41986
|
online: opts.isOnline(),
|
|
41888
41987
|
unread: currentTally().total,
|
|
@@ -41944,6 +42043,19 @@ async function startControlServer(opts) {
|
|
|
41944
42043
|
case "subscribe":
|
|
41945
42044
|
c2.subscribed = true;
|
|
41946
42045
|
return reply(c2, okRes(id, { subscribed: true }));
|
|
42046
|
+
case "history": {
|
|
42047
|
+
if (!opts.fetchHistory) return fail3(c2, id, "unknown_method", "history is not available from this endpoint");
|
|
42048
|
+
const withUser = typeof params.with === "string" ? params.with : void 0;
|
|
42049
|
+
const groupId = typeof params.groupId === "string" ? params.groupId : void 0;
|
|
42050
|
+
if (!withUser && !groupId) return fail3(c2, id, "bad_request", "history needs `with` (a username) or `groupId`");
|
|
42051
|
+
const limit = Math.min(Math.max(Number(params.limit) || 50, 1), 200);
|
|
42052
|
+
try {
|
|
42053
|
+
const messages2 = await opts.fetchHistory({ with: withUser, groupId, limit });
|
|
42054
|
+
return reply(c2, okRes(id, { messages: messages2 }));
|
|
42055
|
+
} catch (e) {
|
|
42056
|
+
return fail3(c2, id, "internal", `history failed: ${e?.message ?? e}`);
|
|
42057
|
+
}
|
|
42058
|
+
}
|
|
41947
42059
|
case "inbox": {
|
|
41948
42060
|
const limit = Math.min(Math.max(Number(params.limit) || 50, 1), MAX_STORED);
|
|
41949
42061
|
return reply(c2, okRes(id, { messages: messages.slice(-limit).reverse() }));
|
|
@@ -42059,7 +42171,14 @@ async function startControlServer(opts) {
|
|
|
42059
42171
|
pid: process.pid,
|
|
42060
42172
|
server: opts.server,
|
|
42061
42173
|
host: opts.getHost(),
|
|
42174
|
+
// Both, and clearly distinct. `workdir` is the brain's configured folder;
|
|
42175
|
+
// `cwd` is where this process runs. The descriptor previously carried only
|
|
42176
|
+
// `workdir` under a name every reader took to mean "where it runs" — which
|
|
42177
|
+
// is how a stale setting ended up being handed out as the answer to "where
|
|
42178
|
+
// do I go to restart this?".
|
|
42062
42179
|
workdir: opts.workdir,
|
|
42180
|
+
cwd: opts.cwd ?? null,
|
|
42181
|
+
machine: opts.machine ?? null,
|
|
42063
42182
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
42064
42183
|
});
|
|
42065
42184
|
writeUnreadFor(self, 0);
|
|
@@ -42139,9 +42258,150 @@ async function startControlServer(opts) {
|
|
|
42139
42258
|
init_popup();
|
|
42140
42259
|
|
|
42141
42260
|
// src/doctor.ts
|
|
42142
|
-
import
|
|
42261
|
+
import cp8 from "node:child_process";
|
|
42143
42262
|
init_version();
|
|
42144
42263
|
init_banner();
|
|
42264
|
+
|
|
42265
|
+
// src/fdcheck.ts
|
|
42266
|
+
import cp6 from "node:child_process";
|
|
42267
|
+
function labelFor(command) {
|
|
42268
|
+
if (/tsx[/\\]dist[/\\]cli\.mjs\s+watch/.test(command)) return "tsx watch dev server";
|
|
42269
|
+
if (/next(-server)?\b.*\bdev\b/.test(command) || /next dev/.test(command)) return "next dev server";
|
|
42270
|
+
if (/nodemon/.test(command)) return "nodemon";
|
|
42271
|
+
if (/webpack|vite/.test(command)) return "bundler dev server";
|
|
42272
|
+
if (/jefrichat-mcp/.test(command)) return "jefri connector";
|
|
42273
|
+
const bin = command.split(/\s+/)[1] ?? command.split(/\s+/)[0] ?? command;
|
|
42274
|
+
return bin.split("/").pop() || "process";
|
|
42275
|
+
}
|
|
42276
|
+
function fdAdvice(hogs, opts) {
|
|
42277
|
+
if (!hogs.length) return null;
|
|
42278
|
+
const total = hogs.reduce((n, h) => n + h.fds, 0);
|
|
42279
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
42280
|
+
for (const h of hogs) {
|
|
42281
|
+
const cur = byLabel.get(h.label) ?? { count: 0, fds: 0 };
|
|
42282
|
+
byLabel.set(h.label, { count: cur.count + 1, fds: cur.fds + h.fds });
|
|
42283
|
+
}
|
|
42284
|
+
const worst = [...byLabel.entries()].sort((a, b) => b[1].fds - a[1].fds)[0];
|
|
42285
|
+
const [label, agg] = worst;
|
|
42286
|
+
const manyDuplicates = agg.count >= 4;
|
|
42287
|
+
const heavy = total > 2e4 || agg.fds > opts.softLimit * 4;
|
|
42288
|
+
if (!manyDuplicates && !heavy) return null;
|
|
42289
|
+
const plural = agg.count === 1 ? "" : "es";
|
|
42290
|
+
return `${agg.count} "${label}" process${plural} are holding ~${agg.fds.toLocaleString()} open files. File watching (and eventually new connections) can fail with EMFILE while they run.`;
|
|
42291
|
+
}
|
|
42292
|
+
function probeFdHogs(limit = 40) {
|
|
42293
|
+
if (process.platform === "win32") return [];
|
|
42294
|
+
try {
|
|
42295
|
+
const out = cp6.execSync("lsof -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -rn | head -" + limit, {
|
|
42296
|
+
encoding: "utf8",
|
|
42297
|
+
timeout: 8e3,
|
|
42298
|
+
maxBuffer: 8 * 1024 * 1024
|
|
42299
|
+
});
|
|
42300
|
+
const hogs = [];
|
|
42301
|
+
for (const line of out.split("\n")) {
|
|
42302
|
+
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
42303
|
+
if (!m) continue;
|
|
42304
|
+
const fds = Number(m[1]);
|
|
42305
|
+
const pid = Number(m[2]);
|
|
42306
|
+
if (fds < 500) continue;
|
|
42307
|
+
let command = "";
|
|
42308
|
+
try {
|
|
42309
|
+
command = cp6.execSync(`ps -p ${pid} -o command=`, { encoding: "utf8", timeout: 2e3 }).trim();
|
|
42310
|
+
} catch {
|
|
42311
|
+
continue;
|
|
42312
|
+
}
|
|
42313
|
+
if (!command) continue;
|
|
42314
|
+
hogs.push({ pid, fds, label: labelFor(command) });
|
|
42315
|
+
}
|
|
42316
|
+
return hogs;
|
|
42317
|
+
} catch {
|
|
42318
|
+
return [];
|
|
42319
|
+
}
|
|
42320
|
+
}
|
|
42321
|
+
function softFdLimit() {
|
|
42322
|
+
try {
|
|
42323
|
+
const n = Number(cp6.execSync("ulimit -n", { encoding: "utf8", shell: "/bin/sh", timeout: 2e3 }).trim());
|
|
42324
|
+
return Number.isFinite(n) && n > 0 ? n : 256;
|
|
42325
|
+
} catch {
|
|
42326
|
+
return 256;
|
|
42327
|
+
}
|
|
42328
|
+
}
|
|
42329
|
+
|
|
42330
|
+
// src/setup.ts
|
|
42331
|
+
init_which();
|
|
42332
|
+
import cp7 from "node:child_process";
|
|
42333
|
+
import readline from "node:readline";
|
|
42334
|
+
var LINUX_PKG = { "terminal-notifier": "" };
|
|
42335
|
+
function resolveInstall(tool, platform, has = isOnPath) {
|
|
42336
|
+
if (has(tool)) return { kind: "present", command: "" };
|
|
42337
|
+
if (platform === "darwin") {
|
|
42338
|
+
return has("brew") ? { kind: "brew", command: `brew install ${tool}` } : { kind: "brew-missing", command: `install Homebrew first (https://brew.sh), then: brew install ${tool}` };
|
|
42339
|
+
}
|
|
42340
|
+
if (LINUX_PKG[tool] === "") {
|
|
42341
|
+
return { kind: "windows", command: `${tool} is macOS-only \u2014 notifications on this platform are informational` };
|
|
42342
|
+
}
|
|
42343
|
+
if (platform === "win32") {
|
|
42344
|
+
return { kind: "windows", command: `${tool} has no native Windows build \u2014 use WSL, or skip it` };
|
|
42345
|
+
}
|
|
42346
|
+
const mgr = ["apt-get", "apt", "dnf", "yum", "pacman", "zypper", "apk"].find(has);
|
|
42347
|
+
const command = mgr === "pacman" ? `sudo pacman -S ${tool}` : mgr === "apk" ? `sudo apk add ${tool}` : mgr ? `sudo ${mgr} install ${tool}` : `install ${tool} with your distro's package manager`;
|
|
42348
|
+
return { kind: "linux-manual", command };
|
|
42349
|
+
}
|
|
42350
|
+
async function promptYesNoDefaultYes(question) {
|
|
42351
|
+
if (!process.stdin.isTTY) return false;
|
|
42352
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
42353
|
+
try {
|
|
42354
|
+
const ans = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
|
|
42355
|
+
return ans === "" || ans === "y" || ans === "yes";
|
|
42356
|
+
} finally {
|
|
42357
|
+
rl.close();
|
|
42358
|
+
}
|
|
42359
|
+
}
|
|
42360
|
+
async function ensureToolInteractive(opts) {
|
|
42361
|
+
const out = opts.out ?? process.stderr;
|
|
42362
|
+
const plan = resolveInstall(opts.tool, process.platform);
|
|
42363
|
+
if (plan.kind === "present") return true;
|
|
42364
|
+
out.write(`
|
|
42365
|
+
\u{1F427} ${opts.why}
|
|
42366
|
+
`);
|
|
42367
|
+
if (plan.kind !== "brew") {
|
|
42368
|
+
out.write(` ${plan.command}
|
|
42369
|
+
`);
|
|
42370
|
+
if (opts.thenWhat) out.write(` ${opts.thenWhat}
|
|
42371
|
+
`);
|
|
42372
|
+
return false;
|
|
42373
|
+
}
|
|
42374
|
+
const yes = await promptYesNoDefaultYes(` Install ${opts.tool} now with Homebrew? (no password, no sudo) [Y/n] `);
|
|
42375
|
+
if (!yes) {
|
|
42376
|
+
out.write(` No problem \u2014 any time: ${plan.command}
|
|
42377
|
+
`);
|
|
42378
|
+
return false;
|
|
42379
|
+
}
|
|
42380
|
+
out.write(` Installing ${opts.tool}\u2026
|
|
42381
|
+
`);
|
|
42382
|
+
try {
|
|
42383
|
+
cp7.execFileSync("brew", ["install", opts.tool], { stdio: "inherit" });
|
|
42384
|
+
} catch {
|
|
42385
|
+
out.write(` \u26A0\uFE0F '${plan.command}' didn't finish \u2014 run it yourself, then try again.
|
|
42386
|
+
`);
|
|
42387
|
+
return false;
|
|
42388
|
+
}
|
|
42389
|
+
if (isOnPath(opts.tool)) {
|
|
42390
|
+
out.write(` \u2705 ${opts.tool} installed.
|
|
42391
|
+
`);
|
|
42392
|
+
return true;
|
|
42393
|
+
}
|
|
42394
|
+
out.write(` \u26A0\uFE0F ${opts.tool} isn't on PATH yet \u2014 open a new terminal and re-run.
|
|
42395
|
+
`);
|
|
42396
|
+
return false;
|
|
42397
|
+
}
|
|
42398
|
+
var WHY = {
|
|
42399
|
+
"terminal-notifier": 'Clicking a Jefri notification can open a reply box, so you answer without\n going through your agent. macOS needs "terminal-notifier" for that \u2014 without\n it the banner still appears, but clicking it does nothing.',
|
|
42400
|
+
tmux: 'Autonomous "session" mode types incoming messages straight into your LIVE\n agent window so you can watch it work, and gives the reply popup somewhere\n nicer to open. Both need "tmux", a small standard terminal tool.'
|
|
42401
|
+
};
|
|
42402
|
+
|
|
42403
|
+
// src/doctor.ts
|
|
42404
|
+
init_which();
|
|
42145
42405
|
init_popup();
|
|
42146
42406
|
import fs10 from "node:fs";
|
|
42147
42407
|
var T = !!process.stdout.isTTY;
|
|
@@ -42173,7 +42433,7 @@ function mask(t) {
|
|
|
42173
42433
|
}
|
|
42174
42434
|
function hasCli(cmd) {
|
|
42175
42435
|
try {
|
|
42176
|
-
const r =
|
|
42436
|
+
const r = cp8.spawnSync(cmd, ["--version"], {
|
|
42177
42437
|
encoding: "utf8",
|
|
42178
42438
|
timeout: 5e3,
|
|
42179
42439
|
shell: process.platform === "win32"
|
|
@@ -42267,6 +42527,18 @@ async function runDoctor() {
|
|
|
42267
42527
|
const runMode2 = auto.mode === "session" ? auto.sessionReady ? "session (types into your live jefri-managed tmux session)" : "session (\u26A0\uFE0F not a jefri-managed tmux session \u2014 falls back to headless; launch via `jefrichat-mcp run <agent>`)" : auto.mode === "interval" ? `interval (batches every ${auto.intervalMinutes}m)` : "headless (one-shot per message)";
|
|
42268
42528
|
info(`Autonomous mode is ON \u2014 ${runMode2}, brain "${auto.brain}" in ${auto.workdir} (jefri_autonomous to change)`);
|
|
42269
42529
|
}
|
|
42530
|
+
try {
|
|
42531
|
+
const hogs = probeFdHogs();
|
|
42532
|
+
const advice = fdAdvice(hogs, { softLimit: softFdLimit() });
|
|
42533
|
+
if (advice) {
|
|
42534
|
+
const worst = hogs.sort((a, b) => b.fds - a.fds)[0];
|
|
42535
|
+
warn(
|
|
42536
|
+
advice,
|
|
42537
|
+
`biggest: pid ${worst.pid} (${worst.fds.toLocaleString()} files). List them with: lsof -n -P | awk '{print $2}' | sort | uniq -c | sort -rn | head`
|
|
42538
|
+
);
|
|
42539
|
+
}
|
|
42540
|
+
} catch {
|
|
42541
|
+
}
|
|
42270
42542
|
console.log("");
|
|
42271
42543
|
console.log(
|
|
42272
42544
|
` ${C.b}\u{1F4EC} ${panelInvocation(hasCli)}${C.x} ${C.d}\u2014 read your messages and reply WITHOUT going through your agent${C.x}`
|
|
@@ -42280,6 +42552,22 @@ async function runDoctor() {
|
|
|
42280
42552
|
if (fs10.existsSync(app))
|
|
42281
42553
|
console.log(` ${C.d}no terminal? open "Jefri Chat" from Spotlight (or drag ${app} to your Dock)${C.x}`);
|
|
42282
42554
|
}
|
|
42555
|
+
if (process.stdin.isTTY) {
|
|
42556
|
+
if (macNeedsTerminalNotifier && getPrefs().enabled) {
|
|
42557
|
+
await ensureToolInteractive({
|
|
42558
|
+
tool: "terminal-notifier",
|
|
42559
|
+
why: WHY["terminal-notifier"],
|
|
42560
|
+
thenWhat: `Then re-run: ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} doctor`
|
|
42561
|
+
});
|
|
42562
|
+
}
|
|
42563
|
+
if (auto.enabled && auto.mode === "session" && !isOnPath("tmux")) {
|
|
42564
|
+
await ensureToolInteractive({
|
|
42565
|
+
tool: "tmux",
|
|
42566
|
+
why: WHY.tmux,
|
|
42567
|
+
thenWhat: `Then launch with: JEFRI_EXPERIMENTAL_SESSION=1 ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} run <agent>`
|
|
42568
|
+
});
|
|
42569
|
+
}
|
|
42570
|
+
}
|
|
42283
42571
|
console.log("");
|
|
42284
42572
|
if (problems === 0 && warnings === 0) {
|
|
42285
42573
|
console.log(`${C.g}${C.b}All good \u2014 you're ready to chat.${C.x}
|
|
@@ -42337,7 +42625,12 @@ function ensureClient() {
|
|
|
42337
42625
|
if (!clientPromise) {
|
|
42338
42626
|
clientPromise = (async () => {
|
|
42339
42627
|
const known = TOKEN ?? readTokenCache()[cacheKey];
|
|
42340
|
-
const clientInfo = {
|
|
42628
|
+
const clientInfo = {
|
|
42629
|
+
name: "jefrichat-mcp",
|
|
42630
|
+
version: connectorVersion(),
|
|
42631
|
+
cwd: process.cwd(),
|
|
42632
|
+
machine: os7.hostname()
|
|
42633
|
+
};
|
|
42341
42634
|
const provision = () => JefriClient.connect({
|
|
42342
42635
|
server: SERVER,
|
|
42343
42636
|
username: USERNAME,
|
|
@@ -42410,6 +42703,8 @@ function ensureClient() {
|
|
|
42410
42703
|
owner: c2.identity?.owner,
|
|
42411
42704
|
server: SERVER,
|
|
42412
42705
|
workdir: getAuto().workdir,
|
|
42706
|
+
cwd: process.cwd(),
|
|
42707
|
+
machine: os7.hostname(),
|
|
42413
42708
|
getHost: () => detectedHost,
|
|
42414
42709
|
isOnline: () => c2.online,
|
|
42415
42710
|
isAutonomous: () => getAuto().enabled,
|
|
@@ -42418,6 +42713,31 @@ function ensureClient() {
|
|
|
42418
42713
|
return { label: b.resolved, ready: b.ready };
|
|
42419
42714
|
},
|
|
42420
42715
|
sendText: sendAs,
|
|
42716
|
+
// Read a conversation from the HUB, over the socket this process
|
|
42717
|
+
// already holds. The panel's own inbox only has what arrived while
|
|
42718
|
+
// this connector was running, so a freshly-started UI shows nothing
|
|
42719
|
+
// for a conversation that has been going for weeks — which reads as
|
|
42720
|
+
// broken rather than as empty.
|
|
42721
|
+
fetchHistory: async ({ with: other, groupId, limit }) => {
|
|
42722
|
+
const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
|
|
42723
|
+
const p = waitFor(c2, "history", (e) => e.conversationId === convId);
|
|
42724
|
+
c2.history(convId);
|
|
42725
|
+
const res = await p;
|
|
42726
|
+
const raw = (res?.messages ?? []).slice(-limit);
|
|
42727
|
+
return raw.map((m) => ({
|
|
42728
|
+
id: String(m.id ?? ""),
|
|
42729
|
+
// Raw username for routing, sanitized copy for display — the same
|
|
42730
|
+
// split the panel makes, for the same reason: a name can carry a
|
|
42731
|
+
// bidi override that would reorder the line around it.
|
|
42732
|
+
from: String(m.senderUsername ?? ""),
|
|
42733
|
+
fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
|
|
42734
|
+
text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
|
|
42735
|
+
at: String(m.createdAt ?? ""),
|
|
42736
|
+
mine: String(m.senderUsername ?? "") === self,
|
|
42737
|
+
kind: m.kind === "file" ? "file" : "text",
|
|
42738
|
+
fileName: m.fileName ?? null
|
|
42739
|
+
}));
|
|
42740
|
+
},
|
|
42421
42741
|
handleWithBrain: async (m) => {
|
|
42422
42742
|
const text5 = await runOnceForMessage({
|
|
42423
42743
|
content: m.text,
|
|
@@ -42601,6 +42921,20 @@ async function main() {
|
|
|
42601
42921
|
};
|
|
42602
42922
|
process.stdin.once("end", shutdownOnHostClose);
|
|
42603
42923
|
process.stdin.once("close", shutdownOnHostClose);
|
|
42924
|
+
process.on("uncaughtException", (err) => {
|
|
42925
|
+
const code3 = err?.code;
|
|
42926
|
+
if (code3 === "EMFILE" || code3 === "ENFILE" || code3 === "EAGAIN") {
|
|
42927
|
+
log(
|
|
42928
|
+
`WARNING: ${code3} \u2014 this machine is out of file descriptors. Something else is holding them (check for abandoned \`tsx watch\`/dev-server processes). Continuing in degraded mode; file watching may not work until pressure drops.`
|
|
42929
|
+
);
|
|
42930
|
+
return;
|
|
42931
|
+
}
|
|
42932
|
+
log("fatal (uncaught):", err?.stack ?? err);
|
|
42933
|
+
process.exit(1);
|
|
42934
|
+
});
|
|
42935
|
+
process.on("unhandledRejection", (reason) => {
|
|
42936
|
+
log("unhandled rejection:", reason?.stack ?? reason);
|
|
42937
|
+
});
|
|
42604
42938
|
void checkForUpdate().then((u) => {
|
|
42605
42939
|
if (u?.outdated)
|
|
42606
42940
|
note(
|
package/package.json
CHANGED