jefrichat-mcp 0.21.0 → 0.28.2
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/README.md +7 -1
- package/dist/http.js +107 -17
- package/dist/index.js +240 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,13 @@ Once connected, the agent gets these `jefri_*` tools:
|
|
|
66
66
|
`jefri_task_status`.
|
|
67
67
|
|
|
68
68
|
**Notifications & autonomy** (local/stdio connector only) — `jefri_notifications`,
|
|
69
|
-
`jefri_autonomous` (owner-only by default).
|
|
69
|
+
`jefri_autonomous` (owner-only by default). In autonomous mode each incoming
|
|
70
|
+
message spawns a headless one-shot "brain" that **auto-matches the host you
|
|
71
|
+
launched from, using that harness's own model** — Codex → `codex exec`,
|
|
72
|
+
Claude Code → `claude -p`, OpenClaw → `openclaw agent exec`, Hermes → `hermes -z`,
|
|
73
|
+
Goose → `goose run -t`. Pin one explicitly with `brain:"codex"` / `"openclaw"` /
|
|
74
|
+
`"hermes"` / `"goose"` / any custom command. If the chosen harness isn't
|
|
75
|
+
installed you get a clear error — it never silently swaps in another model.
|
|
70
76
|
|
|
71
77
|
The doc-memory tools read/write off the local disk, so `jefri_send_file`,
|
|
72
78
|
`jefri_send_folder`, `jefri_add_doc`, `jefri_notifications`, and `jefri_autonomous`
|
package/dist/http.js
CHANGED
|
@@ -49520,7 +49520,8 @@ import os3 from "node:os";
|
|
|
49520
49520
|
var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
|
|
49521
49521
|
var DEFAULTS2 = {
|
|
49522
49522
|
enabled: false,
|
|
49523
|
-
brain: "
|
|
49523
|
+
brain: "auto",
|
|
49524
|
+
// "auto" = match the HOST app (Codex→codex, Claude Code→claude)
|
|
49524
49525
|
workdir: process.cwd(),
|
|
49525
49526
|
replyMode: "mentions",
|
|
49526
49527
|
persona: "",
|
|
@@ -49528,29 +49529,113 @@ var DEFAULTS2 = {
|
|
|
49528
49529
|
ownerOnly: true,
|
|
49529
49530
|
// SECURITY: default to only the owner can direct an autonomous agent
|
|
49530
49531
|
contextMessages: 12,
|
|
49532
|
+
showWorking: true,
|
|
49533
|
+
// visibility: the headless brain is invisible, so tell the chat it's working
|
|
49531
49534
|
promptTemplate: DEFAULT_TEMPLATE
|
|
49532
49535
|
};
|
|
49533
49536
|
var DIR = np2.join(os3.homedir(), ".jefri");
|
|
49534
|
-
var
|
|
49537
|
+
var AUTO_DIR = np2.join(DIR, "autonomous");
|
|
49538
|
+
var LEGACY_FILE = np2.join(DIR, "autonomous.json");
|
|
49535
49539
|
var LOG_FILE2 = np2.join(DIR, "autonomous.log");
|
|
49536
|
-
|
|
49540
|
+
var safeUser = (u) => (u || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
49541
|
+
function cfgPath() {
|
|
49542
|
+
return np2.join(AUTO_DIR, `${safeUser(selfName)}.json`);
|
|
49543
|
+
}
|
|
49544
|
+
function lockPath() {
|
|
49545
|
+
return np2.join(AUTO_DIR, `${safeUser(selfName)}.lock`);
|
|
49546
|
+
}
|
|
49547
|
+
function logLine(line) {
|
|
49537
49548
|
try {
|
|
49538
|
-
|
|
49549
|
+
fs3.mkdirSync(DIR, { recursive: true });
|
|
49550
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
49551
|
+
fs3.appendFileSync(LOG_FILE2, `[${ts}] ${line}
|
|
49552
|
+
`);
|
|
49539
49553
|
} catch {
|
|
49540
|
-
return { ...DEFAULTS2 };
|
|
49541
49554
|
}
|
|
49542
49555
|
}
|
|
49543
|
-
var cfg =
|
|
49556
|
+
var cfg = { ...DEFAULTS2 };
|
|
49557
|
+
function withDerived(c) {
|
|
49558
|
+
return { ...c, resolvedBrain: resolveBrain(c.brain), ownerOnlyEffective: c.ownerOnly || FORCE_OWNER_ONLY, ownerOnlyEnforced: FORCE_OWNER_ONLY };
|
|
49559
|
+
}
|
|
49544
49560
|
function setAuto(patch) {
|
|
49545
49561
|
cfg = { ...cfg, ...patch };
|
|
49562
|
+
if (patch.enabled === true) {
|
|
49563
|
+
cfg.enabledHost = hostName || "unknown app";
|
|
49564
|
+
cfg.enabledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
49565
|
+
}
|
|
49546
49566
|
try {
|
|
49547
|
-
fs3.mkdirSync(
|
|
49548
|
-
fs3.writeFileSync(
|
|
49567
|
+
fs3.mkdirSync(AUTO_DIR, { recursive: true });
|
|
49568
|
+
fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
|
|
49569
|
+
} catch {
|
|
49570
|
+
}
|
|
49571
|
+
if (patch.enabled) acquireResponderLock();
|
|
49572
|
+
return withDerived(cfg);
|
|
49573
|
+
}
|
|
49574
|
+
var hostBrain = "";
|
|
49575
|
+
var hostName = "";
|
|
49576
|
+
function resolveBrain(brain) {
|
|
49577
|
+
if (brain && brain !== "auto") return brain;
|
|
49578
|
+
return hostBrain || "claude";
|
|
49579
|
+
}
|
|
49580
|
+
var selfName = "";
|
|
49581
|
+
var LOCK_STALE_MS = 9e4;
|
|
49582
|
+
var iAmResponder = false;
|
|
49583
|
+
var heartbeat = null;
|
|
49584
|
+
function pidAlive(pid) {
|
|
49585
|
+
try {
|
|
49586
|
+
process.kill(pid, 0);
|
|
49587
|
+
return true;
|
|
49588
|
+
} catch (e) {
|
|
49589
|
+
return e?.code === "EPERM";
|
|
49590
|
+
}
|
|
49591
|
+
}
|
|
49592
|
+
function acquireResponderLock() {
|
|
49593
|
+
try {
|
|
49594
|
+
fs3.mkdirSync(AUTO_DIR, { recursive: true });
|
|
49595
|
+
const now = Date.now();
|
|
49596
|
+
let cur = null;
|
|
49597
|
+
try {
|
|
49598
|
+
cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
|
|
49599
|
+
} catch {
|
|
49600
|
+
}
|
|
49601
|
+
const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
|
|
49602
|
+
if (heldByOther) {
|
|
49603
|
+
if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
|
|
49604
|
+
iAmResponder = false;
|
|
49605
|
+
} else {
|
|
49606
|
+
fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
|
|
49607
|
+
if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
|
|
49608
|
+
iAmResponder = true;
|
|
49609
|
+
}
|
|
49549
49610
|
} catch {
|
|
49611
|
+
iAmResponder = false;
|
|
49612
|
+
}
|
|
49613
|
+
if (!heartbeat) {
|
|
49614
|
+
heartbeat = setInterval(() => {
|
|
49615
|
+
if (cfg.enabled) acquireResponderLock();
|
|
49616
|
+
}, 3e4);
|
|
49617
|
+
heartbeat.unref?.();
|
|
49618
|
+
const release = () => {
|
|
49619
|
+
try {
|
|
49620
|
+
const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
|
|
49621
|
+
if (c.pid === process.pid) fs3.unlinkSync(lockPath());
|
|
49622
|
+
} catch {
|
|
49623
|
+
}
|
|
49624
|
+
};
|
|
49625
|
+
process.once("exit", release);
|
|
49626
|
+
process.once("SIGINT", () => {
|
|
49627
|
+
release();
|
|
49628
|
+
process.exit(0);
|
|
49629
|
+
});
|
|
49630
|
+
process.once("SIGTERM", () => {
|
|
49631
|
+
release();
|
|
49632
|
+
process.exit(0);
|
|
49633
|
+
});
|
|
49550
49634
|
}
|
|
49551
|
-
return
|
|
49635
|
+
return iAmResponder;
|
|
49552
49636
|
}
|
|
49553
49637
|
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
49638
|
+
var FORCE_OWNER_ONLY = true;
|
|
49554
49639
|
|
|
49555
49640
|
// src/tools.ts
|
|
49556
49641
|
var MIME = {
|
|
@@ -50793,14 +50878,15 @@ ${cmd}`);
|
|
|
50793
50878
|
description: "Turn autonomous replies on/off for THIS agent. When ON, each incoming message is handled by a 'brain' (claude / codex / a custom command) that does the work in a folder and replies on its own \u2014 so you can, e.g., leave it on and it builds what people ask and answers them. Only works for your own agent (this connector's token). Call with NO arguments to see current settings. \u26A0\uFE0F When on, remote messages drive a tool-enabled agent on your machine \u2014 only keep it on with people you trust, and scope the work folder.",
|
|
50794
50879
|
inputSchema: {
|
|
50795
50880
|
enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
|
|
50796
|
-
brain: external_exports.string().optional().describe("'
|
|
50881
|
+
brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex exec, Claude Code\u2192claude -p, OpenClaw\u2192openclaw agent exec, Hermes\u2192hermes -z, Goose\u2192goose run -t, each using that harness's own model), or force 'claude' / 'codex' / 'openclaw' / 'hermes' / 'goose' / a full custom command"),
|
|
50797
50882
|
workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
|
|
50798
50883
|
replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
|
|
50799
50884
|
persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
|
|
50800
50885
|
promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
|
|
50801
|
-
ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER
|
|
50886
|
+
ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER. NOTE: during beta this is FORCE-ENABLED for safety (a remote message drives a tool-agent on your machine) \u2014 setting false is accepted but has no effect yet."),
|
|
50802
50887
|
contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
|
|
50803
|
-
replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
|
|
50888
|
+
replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
|
|
50889
|
+
showWorking: external_exports.boolean().optional().describe("post a quick '\u{1F427} on it\u2026' when the brain starts, so the invisible headless run is visible in chat (default on)")
|
|
50804
50890
|
}
|
|
50805
50891
|
},
|
|
50806
50892
|
async (args) => withClient(async () => {
|
|
@@ -50809,7 +50895,8 @@ ${cmd}`);
|
|
|
50809
50895
|
if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
|
|
50810
50896
|
if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
|
|
50811
50897
|
if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
|
|
50812
|
-
if (typeof args.
|
|
50898
|
+
if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
|
|
50899
|
+
if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
|
|
50813
50900
|
if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
|
|
50814
50901
|
if (typeof args.persona === "string") patch.persona = args.persona;
|
|
50815
50902
|
if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
|
|
@@ -50817,21 +50904,24 @@ ${cmd}`);
|
|
|
50817
50904
|
if (args.replyMode) patch.replyMode = args.replyMode;
|
|
50818
50905
|
const c = setAuto(patch);
|
|
50819
50906
|
const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
|
|
50907
|
+
const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
|
|
50908
|
+
const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
|
|
50820
50909
|
const lines = [
|
|
50821
|
-
`\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
|
|
50822
|
-
` Brain: ${
|
|
50910
|
+
`\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
|
|
50911
|
+
` Brain: ${brainLabel}`,
|
|
50823
50912
|
` Work folder: ${c.workdir}`,
|
|
50824
50913
|
` Replies to: ${modeLabel}`,
|
|
50825
|
-
` Owner-only: ${
|
|
50914
|
+
` Owner-only: ${ownerLabel}`,
|
|
50826
50915
|
` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
|
|
50827
50916
|
` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
|
|
50917
|
+
` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
|
|
50828
50918
|
` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
|
|
50829
50919
|
` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
|
|
50830
50920
|
` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
|
|
50831
50921
|
];
|
|
50832
50922
|
if (c.enabled)
|
|
50833
50923
|
lines.push(`
|
|
50834
|
-
\u26A0\uFE0F Incoming messages now drive "${
|
|
50924
|
+
\u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
|
|
50835
50925
|
return ok(lines.join("\n"));
|
|
50836
50926
|
})
|
|
50837
50927
|
);
|
package/dist/index.js
CHANGED
|
@@ -25494,7 +25494,8 @@ import os3 from "node:os";
|
|
|
25494
25494
|
var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
|
|
25495
25495
|
var DEFAULTS2 = {
|
|
25496
25496
|
enabled: false,
|
|
25497
|
-
brain: "
|
|
25497
|
+
brain: "auto",
|
|
25498
|
+
// "auto" = match the HOST app (Codex→codex, Claude Code→claude)
|
|
25498
25499
|
workdir: process.cwd(),
|
|
25499
25500
|
replyMode: "mentions",
|
|
25500
25501
|
persona: "",
|
|
@@ -25502,11 +25503,21 @@ var DEFAULTS2 = {
|
|
|
25502
25503
|
ownerOnly: true,
|
|
25503
25504
|
// SECURITY: default to only the owner can direct an autonomous agent
|
|
25504
25505
|
contextMessages: 12,
|
|
25506
|
+
showWorking: true,
|
|
25507
|
+
// visibility: the headless brain is invisible, so tell the chat it's working
|
|
25505
25508
|
promptTemplate: DEFAULT_TEMPLATE
|
|
25506
25509
|
};
|
|
25507
25510
|
var DIR = np2.join(os3.homedir(), ".jefri");
|
|
25508
|
-
var
|
|
25511
|
+
var AUTO_DIR = np2.join(DIR, "autonomous");
|
|
25512
|
+
var LEGACY_FILE = np2.join(DIR, "autonomous.json");
|
|
25509
25513
|
var LOG_FILE2 = np2.join(DIR, "autonomous.log");
|
|
25514
|
+
var safeUser = (u) => (u || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
25515
|
+
function cfgPath() {
|
|
25516
|
+
return np2.join(AUTO_DIR, `${safeUser(selfName)}.json`);
|
|
25517
|
+
}
|
|
25518
|
+
function lockPath() {
|
|
25519
|
+
return np2.join(AUTO_DIR, `${safeUser(selfName)}.lock`);
|
|
25520
|
+
}
|
|
25510
25521
|
function logLine(line) {
|
|
25511
25522
|
try {
|
|
25512
25523
|
fs3.mkdirSync(DIR, { recursive: true });
|
|
@@ -25518,23 +25529,54 @@ function logLine(line) {
|
|
|
25518
25529
|
}
|
|
25519
25530
|
function loadCfg() {
|
|
25520
25531
|
try {
|
|
25521
|
-
return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(
|
|
25532
|
+
return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(cfgPath(), "utf8")) };
|
|
25533
|
+
} catch {
|
|
25534
|
+
}
|
|
25535
|
+
try {
|
|
25536
|
+
const legacy = { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(LEGACY_FILE, "utf8")) };
|
|
25537
|
+
logLine(`migrated legacy autonomous.json \u2192 ${safeUser(selfName)}.json`);
|
|
25538
|
+
return legacy;
|
|
25522
25539
|
} catch {
|
|
25523
25540
|
return { ...DEFAULTS2 };
|
|
25524
25541
|
}
|
|
25525
25542
|
}
|
|
25526
|
-
var cfg =
|
|
25543
|
+
var cfg = { ...DEFAULTS2 };
|
|
25544
|
+
function withDerived(c) {
|
|
25545
|
+
return { ...c, resolvedBrain: resolveBrain(c.brain), ownerOnlyEffective: c.ownerOnly || FORCE_OWNER_ONLY, ownerOnlyEnforced: FORCE_OWNER_ONLY };
|
|
25546
|
+
}
|
|
25527
25547
|
function getAuto() {
|
|
25528
|
-
return
|
|
25548
|
+
return withDerived(cfg);
|
|
25529
25549
|
}
|
|
25530
25550
|
function setAuto(patch) {
|
|
25531
25551
|
cfg = { ...cfg, ...patch };
|
|
25552
|
+
if (patch.enabled === true) {
|
|
25553
|
+
cfg.enabledHost = hostName || "unknown app";
|
|
25554
|
+
cfg.enabledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25555
|
+
}
|
|
25532
25556
|
try {
|
|
25533
|
-
fs3.mkdirSync(
|
|
25534
|
-
fs3.writeFileSync(
|
|
25557
|
+
fs3.mkdirSync(AUTO_DIR, { recursive: true });
|
|
25558
|
+
fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
|
|
25535
25559
|
} catch {
|
|
25536
25560
|
}
|
|
25537
|
-
|
|
25561
|
+
if (patch.enabled) acquireResponderLock();
|
|
25562
|
+
return withDerived(cfg);
|
|
25563
|
+
}
|
|
25564
|
+
var hostBrain = "";
|
|
25565
|
+
var hostName = "";
|
|
25566
|
+
function setAutoHost(clientName) {
|
|
25567
|
+
hostName = clientName ?? "";
|
|
25568
|
+
const n = hostName.toLowerCase();
|
|
25569
|
+
if (n.includes("codex")) hostBrain = "codex";
|
|
25570
|
+
else if (n.includes("claude")) hostBrain = "claude";
|
|
25571
|
+
else if (n.includes("openclaw")) hostBrain = "openclaw";
|
|
25572
|
+
else if (n.includes("hermes")) hostBrain = "hermes";
|
|
25573
|
+
else if (n.includes("goose")) hostBrain = "goose";
|
|
25574
|
+
else hostBrain = "";
|
|
25575
|
+
logLine(`host detected: "${hostName}" \u2192 brain ${hostBrain || "(unknown, default claude)"}`);
|
|
25576
|
+
}
|
|
25577
|
+
function resolveBrain(brain) {
|
|
25578
|
+
if (brain && brain !== "auto") return brain;
|
|
25579
|
+
return hostBrain || "claude";
|
|
25538
25580
|
}
|
|
25539
25581
|
var selfName = "";
|
|
25540
25582
|
var ownerName = "";
|
|
@@ -25543,12 +25585,125 @@ function configureAuto(opts) {
|
|
|
25543
25585
|
selfName = opts.self;
|
|
25544
25586
|
ownerName = opts.owner ?? "";
|
|
25545
25587
|
fetchHistory = opts.getHistory;
|
|
25588
|
+
cfg = loadCfg();
|
|
25589
|
+
if (cfg.enabled) acquireResponderLock();
|
|
25590
|
+
watchConfig();
|
|
25591
|
+
}
|
|
25592
|
+
var watcher = null;
|
|
25593
|
+
var watchDebounce = null;
|
|
25594
|
+
function watchConfig() {
|
|
25595
|
+
try {
|
|
25596
|
+
watcher?.close();
|
|
25597
|
+
} catch {
|
|
25598
|
+
}
|
|
25599
|
+
try {
|
|
25600
|
+
fs3.mkdirSync(AUTO_DIR, { recursive: true });
|
|
25601
|
+
const file = `${safeUser(selfName)}.json`;
|
|
25602
|
+
watcher = fs3.watch(AUTO_DIR, { persistent: false }, (_evt, name) => {
|
|
25603
|
+
if (name && name !== file) return;
|
|
25604
|
+
if (watchDebounce) clearTimeout(watchDebounce);
|
|
25605
|
+
watchDebounce = setTimeout(() => {
|
|
25606
|
+
const wasEnabled = cfg.enabled;
|
|
25607
|
+
cfg = loadCfg();
|
|
25608
|
+
if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
|
|
25609
|
+
if (cfg.enabled) acquireResponderLock();
|
|
25610
|
+
else releaseResponderLock();
|
|
25611
|
+
}, 200);
|
|
25612
|
+
});
|
|
25613
|
+
watcher.unref?.();
|
|
25614
|
+
} catch {
|
|
25615
|
+
}
|
|
25616
|
+
}
|
|
25617
|
+
var LOCK_STALE_MS = 9e4;
|
|
25618
|
+
var iAmResponder = false;
|
|
25619
|
+
var heartbeat = null;
|
|
25620
|
+
function pidAlive(pid) {
|
|
25621
|
+
try {
|
|
25622
|
+
process.kill(pid, 0);
|
|
25623
|
+
return true;
|
|
25624
|
+
} catch (e) {
|
|
25625
|
+
return e?.code === "EPERM";
|
|
25626
|
+
}
|
|
25627
|
+
}
|
|
25628
|
+
function acquireResponderLock() {
|
|
25629
|
+
try {
|
|
25630
|
+
fs3.mkdirSync(AUTO_DIR, { recursive: true });
|
|
25631
|
+
const now = Date.now();
|
|
25632
|
+
let cur = null;
|
|
25633
|
+
try {
|
|
25634
|
+
cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
|
|
25635
|
+
} catch {
|
|
25636
|
+
}
|
|
25637
|
+
const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
|
|
25638
|
+
if (heldByOther) {
|
|
25639
|
+
if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
|
|
25640
|
+
iAmResponder = false;
|
|
25641
|
+
} else {
|
|
25642
|
+
fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
|
|
25643
|
+
if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
|
|
25644
|
+
iAmResponder = true;
|
|
25645
|
+
}
|
|
25646
|
+
} catch {
|
|
25647
|
+
iAmResponder = false;
|
|
25648
|
+
}
|
|
25649
|
+
if (!heartbeat) {
|
|
25650
|
+
heartbeat = setInterval(() => {
|
|
25651
|
+
if (cfg.enabled) acquireResponderLock();
|
|
25652
|
+
}, 3e4);
|
|
25653
|
+
heartbeat.unref?.();
|
|
25654
|
+
const release = () => {
|
|
25655
|
+
try {
|
|
25656
|
+
const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
|
|
25657
|
+
if (c.pid === process.pid) fs3.unlinkSync(lockPath());
|
|
25658
|
+
} catch {
|
|
25659
|
+
}
|
|
25660
|
+
};
|
|
25661
|
+
process.once("exit", release);
|
|
25662
|
+
process.once("SIGINT", () => {
|
|
25663
|
+
release();
|
|
25664
|
+
process.exit(0);
|
|
25665
|
+
});
|
|
25666
|
+
process.once("SIGTERM", () => {
|
|
25667
|
+
release();
|
|
25668
|
+
process.exit(0);
|
|
25669
|
+
});
|
|
25670
|
+
}
|
|
25671
|
+
return iAmResponder;
|
|
25672
|
+
}
|
|
25673
|
+
function releaseResponderLock() {
|
|
25674
|
+
iAmResponder = false;
|
|
25675
|
+
try {
|
|
25676
|
+
const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
|
|
25677
|
+
if (c.pid === process.pid) fs3.unlinkSync(lockPath());
|
|
25678
|
+
} catch {
|
|
25679
|
+
}
|
|
25546
25680
|
}
|
|
25547
25681
|
var expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
|
|
25682
|
+
function tokenize(cmd) {
|
|
25683
|
+
const out = [];
|
|
25684
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
25685
|
+
let m;
|
|
25686
|
+
while ((m = re.exec(cmd)) !== null) out.push(m[1] ?? m[2] ?? m[3]);
|
|
25687
|
+
return out;
|
|
25688
|
+
}
|
|
25689
|
+
function brainExecutable(brain) {
|
|
25690
|
+
const b = (brain || "").trim();
|
|
25691
|
+
if (!b || b === "auto") return null;
|
|
25692
|
+
if (b === "claude") return "claude";
|
|
25693
|
+
if (b === "codex") return "codex";
|
|
25694
|
+
if (b === "openclaw") return "openclaw";
|
|
25695
|
+
if (b === "hermes") return "hermes";
|
|
25696
|
+
if (b === "goose") return "goose";
|
|
25697
|
+
return tokenize(b)[0] || null;
|
|
25698
|
+
}
|
|
25548
25699
|
function brainArgv(brain, prompt) {
|
|
25549
|
-
|
|
25550
|
-
if (
|
|
25551
|
-
|
|
25700
|
+
const b = resolveBrain(brain);
|
|
25701
|
+
if (b === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
|
|
25702
|
+
if (b === "codex") return ["codex", ["exec", "--full-auto", prompt]];
|
|
25703
|
+
if (b === "openclaw") return ["openclaw", ["agent", "exec", prompt]];
|
|
25704
|
+
if (b === "hermes") return ["hermes", ["-z", prompt]];
|
|
25705
|
+
if (b === "goose") return ["goose", ["run", "-t", prompt]];
|
|
25706
|
+
const parts = tokenize(b.trim());
|
|
25552
25707
|
return [parts[0], [...parts.slice(1), prompt]];
|
|
25553
25708
|
}
|
|
25554
25709
|
var MAX_PROMPT = 12e3;
|
|
@@ -25557,6 +25712,7 @@ var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
|
25557
25712
|
var FORCE_OWNER_ONLY = true;
|
|
25558
25713
|
function shouldHandle(m) {
|
|
25559
25714
|
if (!cfg.enabled) return false;
|
|
25715
|
+
if (!iAmResponder) return false;
|
|
25560
25716
|
const fromOwner = !!ownerName && m.sender === ownerName;
|
|
25561
25717
|
const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
|
|
25562
25718
|
if (ownerOnly && !fromOwner) return false;
|
|
@@ -25600,7 +25756,11 @@ function runBrain(cmd, args, cwd) {
|
|
|
25600
25756
|
child.stderr?.on("data", (d) => err += d.toString());
|
|
25601
25757
|
child.on("error", (e) => {
|
|
25602
25758
|
clearTimeout(timer);
|
|
25603
|
-
|
|
25759
|
+
if (e?.code === "ENOENT") {
|
|
25760
|
+
reject(new Error(
|
|
25761
|
+
`the "${cmd}" command isn't installed / on PATH, so I can't run it as your autonomous brain. Install ${cmd}, or pin a different brain: jefri_autonomous(brain:"claude"|"codex"|"openclaw agent exec"|"hermes -z"|"goose run -t"|any command).`
|
|
25762
|
+
));
|
|
25763
|
+
} else reject(e);
|
|
25604
25764
|
});
|
|
25605
25765
|
child.on("close", (code) => {
|
|
25606
25766
|
clearTimeout(timer);
|
|
@@ -25635,25 +25795,56 @@ function handleAutonomous(m, reply, log2) {
|
|
|
25635
25795
|
queue.push(async () => {
|
|
25636
25796
|
const persona = cfg.persona ? cfg.persona.trim() + "\n\n" : "";
|
|
25637
25797
|
const context = await buildContext(m.conversationId);
|
|
25638
|
-
const
|
|
25798
|
+
const fromOwner = !!ownerName && m.sender === ownerName;
|
|
25799
|
+
const facts = [
|
|
25800
|
+
`You are @${selfName}, an autonomous agent on Jefri Chat`,
|
|
25801
|
+
`harness/host app: ${hostName || "unknown"}`,
|
|
25802
|
+
`brain: ${resolveBrain(cfg.brain)}${cfg.brain === "auto" ? " (auto-matched to the host)" : ""}`,
|
|
25803
|
+
`owned by: @${ownerName || "(no owner)"}`,
|
|
25804
|
+
cfg.enabledHost ? `autonomous enabled from ${cfg.enabledHost}${cfg.enabledAt ? ` at ${cfg.enabledAt}` : ""}` : "",
|
|
25805
|
+
fromOwner ? `machine: ${os3.hostname()}` : "",
|
|
25806
|
+
fromOwner ? `working folder: ${cfg.workdir}` : ""
|
|
25807
|
+
].filter(Boolean).join("; ");
|
|
25808
|
+
const runtime = `[Runtime facts \u2014 if asked who set you up or where you're running, answer from THESE, don't guess: ${facts}.]
|
|
25809
|
+
|
|
25810
|
+
`;
|
|
25811
|
+
const prompt = (runtime + (cfg.promptTemplate || DEFAULT_TEMPLATE).replace(/\{persona\}/g, persona).replace(/\{me\}/g, selfName).replace(/\{sender\}/g, m.sender).replace(/\{where\}/g, where).replace(/\{context\}/g, context).replace(/\{message\}/g, m.content)).slice(0, MAX_PROMPT);
|
|
25639
25812
|
const [cmd, args] = brainArgv(cfg.brain, prompt);
|
|
25640
|
-
|
|
25641
|
-
|
|
25813
|
+
const shownBrain = resolveBrain(cfg.brain) + (cfg.brain === "auto" ? " (auto \u2192 host)" : "");
|
|
25814
|
+
log2(`autonomous: @${m.sender} \u2192 running "${shownBrain}"\u2026`);
|
|
25815
|
+
logLine(` running: ${shownBrain} in ${cfg.workdir}`);
|
|
25816
|
+
if (cfg.showWorking) {
|
|
25817
|
+
try {
|
|
25818
|
+
reply(`\u{1F427} on it\u2026 (${resolveBrain(cfg.brain)})`);
|
|
25819
|
+
} catch {
|
|
25820
|
+
}
|
|
25821
|
+
}
|
|
25642
25822
|
try {
|
|
25643
25823
|
const out = await runBrain(cmd, args, cfg.workdir);
|
|
25644
25824
|
const text = (out.trim() || "(done)").slice(0, MAX_REPLY);
|
|
25645
|
-
reply
|
|
25646
|
-
log2(`autonomous: replied to @${m.sender}`);
|
|
25647
|
-
logLine(` \u2192 replied: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
|
|
25825
|
+
const delivered = await deliverReply(reply, text);
|
|
25826
|
+
log2(`autonomous: ${delivered ? "replied to" : "reply FAILED (socket down) for"} @${m.sender}`);
|
|
25827
|
+
logLine(` ${delivered ? "\u2192 replied" : "\u2717 reply lost (socket down after retries)"}: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
|
|
25648
25828
|
} catch (e) {
|
|
25649
25829
|
const msg = `(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY);
|
|
25650
|
-
reply
|
|
25830
|
+
await deliverReply(reply, msg);
|
|
25651
25831
|
log2(`autonomous: failed for @${m.sender}: ${e?.message ?? e}`);
|
|
25652
25832
|
logLine(` \u2717 failed: ${e?.message ?? e}`);
|
|
25653
25833
|
}
|
|
25654
25834
|
});
|
|
25655
25835
|
void drain();
|
|
25656
25836
|
}
|
|
25837
|
+
async function deliverReply(reply, text) {
|
|
25838
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
25839
|
+
try {
|
|
25840
|
+
reply(text);
|
|
25841
|
+
return true;
|
|
25842
|
+
} catch {
|
|
25843
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
25844
|
+
}
|
|
25845
|
+
}
|
|
25846
|
+
return false;
|
|
25847
|
+
}
|
|
25657
25848
|
|
|
25658
25849
|
// src/tools.ts
|
|
25659
25850
|
var MIME = {
|
|
@@ -26896,14 +27087,15 @@ ${cmd}`);
|
|
|
26896
27087
|
description: "Turn autonomous replies on/off for THIS agent. When ON, each incoming message is handled by a 'brain' (claude / codex / a custom command) that does the work in a folder and replies on its own \u2014 so you can, e.g., leave it on and it builds what people ask and answers them. Only works for your own agent (this connector's token). Call with NO arguments to see current settings. \u26A0\uFE0F When on, remote messages drive a tool-enabled agent on your machine \u2014 only keep it on with people you trust, and scope the work folder.",
|
|
26897
27088
|
inputSchema: {
|
|
26898
27089
|
enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
|
|
26899
|
-
brain: external_exports.string().optional().describe("'
|
|
27090
|
+
brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex exec, Claude Code\u2192claude -p, OpenClaw\u2192openclaw agent exec, Hermes\u2192hermes -z, Goose\u2192goose run -t, each using that harness's own model), or force 'claude' / 'codex' / 'openclaw' / 'hermes' / 'goose' / a full custom command"),
|
|
26900
27091
|
workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
|
|
26901
27092
|
replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
|
|
26902
27093
|
persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
|
|
26903
27094
|
promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
|
|
26904
|
-
ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER
|
|
27095
|
+
ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER. NOTE: during beta this is FORCE-ENABLED for safety (a remote message drives a tool-agent on your machine) \u2014 setting false is accepted but has no effect yet."),
|
|
26905
27096
|
contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
|
|
26906
|
-
replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
|
|
27097
|
+
replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
|
|
27098
|
+
showWorking: external_exports.boolean().optional().describe("post a quick '\u{1F427} on it\u2026' when the brain starts, so the invisible headless run is visible in chat (default on)")
|
|
26907
27099
|
}
|
|
26908
27100
|
},
|
|
26909
27101
|
async (args) => withClient(async () => {
|
|
@@ -26912,7 +27104,8 @@ ${cmd}`);
|
|
|
26912
27104
|
if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
|
|
26913
27105
|
if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
|
|
26914
27106
|
if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
|
|
26915
|
-
if (typeof args.
|
|
27107
|
+
if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
|
|
27108
|
+
if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
|
|
26916
27109
|
if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
|
|
26917
27110
|
if (typeof args.persona === "string") patch.persona = args.persona;
|
|
26918
27111
|
if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
|
|
@@ -26920,21 +27113,24 @@ ${cmd}`);
|
|
|
26920
27113
|
if (args.replyMode) patch.replyMode = args.replyMode;
|
|
26921
27114
|
const c = setAuto(patch);
|
|
26922
27115
|
const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
|
|
27116
|
+
const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
|
|
27117
|
+
const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
|
|
26923
27118
|
const lines = [
|
|
26924
|
-
`\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
|
|
26925
|
-
` Brain: ${
|
|
27119
|
+
`\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
|
|
27120
|
+
` Brain: ${brainLabel}`,
|
|
26926
27121
|
` Work folder: ${c.workdir}`,
|
|
26927
27122
|
` Replies to: ${modeLabel}`,
|
|
26928
|
-
` Owner-only: ${
|
|
27123
|
+
` Owner-only: ${ownerLabel}`,
|
|
26929
27124
|
` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
|
|
26930
27125
|
` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
|
|
27126
|
+
` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
|
|
26931
27127
|
` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
|
|
26932
27128
|
` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
|
|
26933
27129
|
` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
|
|
26934
27130
|
];
|
|
26935
27131
|
if (c.enabled)
|
|
26936
27132
|
lines.push(`
|
|
26937
|
-
\u26A0\uFE0F Incoming messages now drive "${
|
|
27133
|
+
\u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
|
|
26938
27134
|
return ok(lines.join("\n"));
|
|
26939
27135
|
})
|
|
26940
27136
|
);
|
|
@@ -27055,9 +27251,17 @@ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
|
|
|
27055
27251
|
info("Skipping hub auth check (no token)");
|
|
27056
27252
|
}
|
|
27057
27253
|
const auto = getAuto();
|
|
27058
|
-
const brainCmd = auto.brain
|
|
27059
|
-
if (
|
|
27060
|
-
|
|
27254
|
+
const brainCmd = brainExecutable(auto.brain);
|
|
27255
|
+
if (brainCmd === null) {
|
|
27256
|
+
const known = ["claude", "codex", "openclaw", "hermes", "goose"];
|
|
27257
|
+
const found = known.filter(hasCli);
|
|
27258
|
+
if (found.length) info(`Autonomous brain "auto" matches your host at runtime \u2014 harnesses on PATH: ${found.join(", ")}`);
|
|
27259
|
+
else info(`Autonomous brain "auto" matches your host at runtime \u2014 none of ${known.join("/")} found on PATH yet (only needed if you enable autonomous mode)`);
|
|
27260
|
+
} else if (hasCli(brainCmd)) {
|
|
27261
|
+
pass(`Autonomous brain "${brainCmd}" found on PATH`);
|
|
27262
|
+
} else {
|
|
27263
|
+
info(`Autonomous brain "${brainCmd}" not on PATH (only needed if you enable autonomous mode)`);
|
|
27264
|
+
}
|
|
27061
27265
|
const prefs2 = getPrefs();
|
|
27062
27266
|
if (!prefs2.enabled) info("Desktop notifications are OFF (use the jefri_notifications tool to turn on)");
|
|
27063
27267
|
else if (macNeedsTerminalNotifier)
|
|
@@ -27229,6 +27433,12 @@ async function main() {
|
|
|
27229
27433
|
}
|
|
27230
27434
|
ensureClient().catch((e) => log("initial connect failed (will retry on first tool):", e?.message ?? e));
|
|
27231
27435
|
const transport = new StdioServerTransport();
|
|
27436
|
+
server.server.oninitialized = () => {
|
|
27437
|
+
try {
|
|
27438
|
+
setAutoHost(server.server.getClientVersion()?.name);
|
|
27439
|
+
} catch {
|
|
27440
|
+
}
|
|
27441
|
+
};
|
|
27232
27442
|
await server.connect(transport);
|
|
27233
27443
|
log(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
|
|
27234
27444
|
}
|
package/package.json
CHANGED