@essentialai/cogent-plugin 3.25.8 → 3.25.10
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/.claude-plugin/plugin.json +1 -1
- package/bridge/cogent-bridge.mjs +202 -37
- package/hooks/check-on-stop.lib.mjs +31 -0
- package/hooks/check-on-stop.mjs +5 -1
- package/hooks/record-session.mjs +67 -2
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogent",
|
|
3
|
-
"version": "3.25.
|
|
3
|
+
"version": "3.25.10",
|
|
4
4
|
"description": "Inter-session communication bridge for Claude Code with Slack integration. Enables CC agents and Slack team members to communicate in real time.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Essential AI Solutions",
|
package/bridge/cogent-bridge.mjs
CHANGED
|
@@ -655,18 +655,80 @@ async function loadCredentials(credentialPath) {
|
|
|
655
655
|
async function saveCredentials(creds, credentialPath) {
|
|
656
656
|
const filePath = resolveCredentialPath(credentialPath);
|
|
657
657
|
await fs4.mkdir(path4.dirname(filePath), { recursive: true, mode: 448 });
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
{
|
|
662
|
-
|
|
663
|
-
|
|
658
|
+
let out = creds;
|
|
659
|
+
if (creds.peers === void 0) {
|
|
660
|
+
const prior = await loadCredentials(filePath);
|
|
661
|
+
if (prior?.peers) out = { ...creds, peers: prior.peers };
|
|
662
|
+
}
|
|
663
|
+
const tmp = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
664
|
+
await fs4.writeFile(tmp, JSON.stringify(out, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
665
|
+
await fs4.chmod(tmp, 384);
|
|
666
|
+
await fs4.rename(tmp, filePath);
|
|
664
667
|
}
|
|
665
668
|
async function rememberIssuedPeerSecret(sessionId, peerId, peerSecret) {
|
|
666
669
|
const creds = await loadCredentials();
|
|
667
670
|
if (!creds || creds.sessionId !== sessionId) return;
|
|
668
|
-
|
|
669
|
-
|
|
671
|
+
const entry = creds.peers?.[peerId];
|
|
672
|
+
const entryCurrent = !entry || entry.sessionId !== sessionId || entry.peerSecret === peerSecret;
|
|
673
|
+
if (creds.peerId === peerId && creds.peerSecret === peerSecret && entryCurrent) return;
|
|
674
|
+
const peers = entry && entry.sessionId === sessionId ? { ...creds.peers, [peerId]: { ...entry, peerSecret, savedAt: (/* @__PURE__ */ new Date()).toISOString() } } : creds.peers;
|
|
675
|
+
const base = entry && entry.sessionId === sessionId ? { ...creds, ...entry } : creds;
|
|
676
|
+
await saveCredentials({ ...base, peerId, peerSecret, ...peers ? { peers } : {}, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
677
|
+
}
|
|
678
|
+
function setCurrentCloudConnection(c2) {
|
|
679
|
+
currentCloudConnection = c2;
|
|
680
|
+
}
|
|
681
|
+
function getCurrentCloudConnection() {
|
|
682
|
+
return currentCloudConnection;
|
|
683
|
+
}
|
|
684
|
+
function peerCredentials(folder, peerId) {
|
|
685
|
+
if (!folder) return null;
|
|
686
|
+
if (folder.peerId === peerId) {
|
|
687
|
+
const { peers: _p, ...top } = folder;
|
|
688
|
+
return top;
|
|
689
|
+
}
|
|
690
|
+
const e = folder.peers?.[peerId];
|
|
691
|
+
return e ? { ...e, peerId } : null;
|
|
692
|
+
}
|
|
693
|
+
function peerRecordsOf(folder) {
|
|
694
|
+
if (!folder?.peers) return [];
|
|
695
|
+
return Object.entries(folder.peers).map(([peerId, e]) => ({ ...e, peerId }));
|
|
696
|
+
}
|
|
697
|
+
async function loadPeerRecord(peerId, credentialPath) {
|
|
698
|
+
return peerCredentials(await loadCredentials(credentialPath), peerId);
|
|
699
|
+
}
|
|
700
|
+
async function loadPeerRecords(credentialPath) {
|
|
701
|
+
return peerRecordsOf(await loadCredentials(credentialPath));
|
|
702
|
+
}
|
|
703
|
+
async function savePeerRecord(rec, credentialPath) {
|
|
704
|
+
const folder = await loadCredentials(credentialPath);
|
|
705
|
+
if (!folder) return;
|
|
706
|
+
const { peerId, peers: _p, ...entry } = rec;
|
|
707
|
+
await saveCredentials({ ...folder, peers: { ...folder.peers ?? {}, [peerId]: entry } }, credentialPath);
|
|
708
|
+
}
|
|
709
|
+
async function deletePeerRecord(peerId, credentialPath) {
|
|
710
|
+
const folder = await loadCredentials(credentialPath);
|
|
711
|
+
if (!folder?.peers?.[peerId]) return;
|
|
712
|
+
const { [peerId]: _gone, ...rest } = folder.peers;
|
|
713
|
+
await saveCredentials({ ...folder, peers: rest }, credentialPath);
|
|
714
|
+
}
|
|
715
|
+
function chooseRestoreCredentials(folder, records, ownSession, platform) {
|
|
716
|
+
if (records.length === 0) return { creds: folder };
|
|
717
|
+
const top = folder?.peerId ? peerCredentials(folder, folder.peerId) : null;
|
|
718
|
+
const pick2 = (r) => peerCredentials(folder, r.peerId) ?? r;
|
|
719
|
+
if (ownSession) {
|
|
720
|
+
const mine = records.filter((r) => r.agentSession === ownSession);
|
|
721
|
+
if (mine.length > 0) return { creds: pick2(mine[0]) };
|
|
722
|
+
}
|
|
723
|
+
const sameKind = records.filter((r) => r.platform === platform);
|
|
724
|
+
if (sameKind.length === 1) return { creds: pick2(sameKind[0]) };
|
|
725
|
+
if (sameKind.length > 1 && top && sameKind.some((r) => r.peerId === top.peerId)) return { creds: top };
|
|
726
|
+
if (sameKind.length === 0 && top && !records.some((r) => r.peerId === top.peerId)) return { creds: top };
|
|
727
|
+
const names = records.map((r) => `'${r.peerId}' (${r.platform ?? "unknown agent"})`).join(", ");
|
|
728
|
+
return {
|
|
729
|
+
creds: null,
|
|
730
|
+
skip: sameKind.length === 0 ? `this folder's saved names belong to other agents \u2014 ${names}; this ${platform} agent has none, so it registers its own name explicitly` : `this folder has several saved ${platform} names and this bridge cannot tell which is its own \u2014 ${names}; the agent registers its name explicitly`
|
|
731
|
+
};
|
|
670
732
|
}
|
|
671
733
|
async function legacyGlobalCredentialsExist() {
|
|
672
734
|
try {
|
|
@@ -676,7 +738,7 @@ async function legacyGlobalCredentialsExist() {
|
|
|
676
738
|
return false;
|
|
677
739
|
}
|
|
678
740
|
}
|
|
679
|
-
var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag, cloudAttemptGeneration, cloudAttemptListeners;
|
|
741
|
+
var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag, cloudAttemptGeneration, cloudAttemptListeners, currentCloudConnection;
|
|
680
742
|
var init_credential_store = __esm({
|
|
681
743
|
"src/cloud/credential-store.ts"() {
|
|
682
744
|
"use strict";
|
|
@@ -688,6 +750,7 @@ var init_credential_store = __esm({
|
|
|
688
750
|
cloudAttemptInProcessFlag = false;
|
|
689
751
|
cloudAttemptGeneration = 0;
|
|
690
752
|
cloudAttemptListeners = /* @__PURE__ */ new Set();
|
|
753
|
+
currentCloudConnection = null;
|
|
691
754
|
}
|
|
692
755
|
});
|
|
693
756
|
|
|
@@ -26412,8 +26475,8 @@ var init_stdio2 = __esm({
|
|
|
26412
26475
|
// src/constants.ts
|
|
26413
26476
|
import { createRequire } from "node:module";
|
|
26414
26477
|
function resolveVersion() {
|
|
26415
|
-
if ("3.25.
|
|
26416
|
-
return "3.25.
|
|
26478
|
+
if ("3.25.10") {
|
|
26479
|
+
return "3.25.10";
|
|
26417
26480
|
}
|
|
26418
26481
|
try {
|
|
26419
26482
|
const require2 = createRequire(import.meta.url);
|
|
@@ -26629,6 +26692,9 @@ ${issues}`);
|
|
|
26629
26692
|
_config = Object.freeze(data);
|
|
26630
26693
|
return _config;
|
|
26631
26694
|
}
|
|
26695
|
+
function readOnlyWake(config2) {
|
|
26696
|
+
return config2.COGENT_READ_ONLY_WAKE || config2.COGENT_WAKE_TOOLS === "read";
|
|
26697
|
+
}
|
|
26632
26698
|
function getConfig() {
|
|
26633
26699
|
if (!_config) throw new Error("Config not loaded. Call loadConfig() first.");
|
|
26634
26700
|
return _config;
|
|
@@ -26692,6 +26758,44 @@ var init_config = __esm({
|
|
|
26692
26758
|
// launches via the daemon (see bin/cogent-codex) and sets this to `app-server`.
|
|
26693
26759
|
// Only affects COGENT_PLATFORM=codex; claude/gemini/mail rails never read it.
|
|
26694
26760
|
COGENT_CODEX_WAKE: import_zod2.z.enum(["auto", "exec-resume", "app-server"]).default("auto"),
|
|
26761
|
+
// COG-86 / COG-88: what a Claude wake may do. `full` (default) = every tool, as before 3.25.9. `read` = Read,
|
|
26762
|
+
// Glob and Grep only: the wake answers but cannot edit, run commands or commit. A wake resumes the SAME session
|
|
26763
|
+
// a human may have open in a terminal, and nothing can tell reliably that the terminal is there; on 2026-10-15
|
|
26764
|
+
// a full-tool wake edited a live session's files, then committed and pushed. 3.25.9 made `read` the default;
|
|
26765
|
+
// the operator reversed that on 2026-10-16 (COG-88): read-only is an opt-in, switched with the environment
|
|
26766
|
+
// variable COGENT_READ_ONLY_WAKE (below) or here. An EMPTY value counts as unset — a host that substitutes an empty
|
|
26767
|
+
// string for a missing setting must not take the bridge down. Case and surrounding spaces are ignored
|
|
26768
|
+
// ("Read", "read " = read), the same rule COGENT_READ_ONLY_WAKE uses — a user opting in must not stop the
|
|
26769
|
+
// bridge over a capital letter. Any other unknown value still fails loudly.
|
|
26770
|
+
// Read it through readOnlyWake(), never directly: either opt-in turns read-only on.
|
|
26771
|
+
COGENT_WAKE_TOOLS: import_zod2.z.preprocess(
|
|
26772
|
+
(v) => {
|
|
26773
|
+
if (typeof v !== "string") return v;
|
|
26774
|
+
const s = v.trim().toLowerCase();
|
|
26775
|
+
return s === "" ? void 0 : s;
|
|
26776
|
+
},
|
|
26777
|
+
import_zod2.z.enum(["read", "full"]).default("full")
|
|
26778
|
+
),
|
|
26779
|
+
// COG-88: the read-only wake opt-in — an ENVIRONMENT VARIABLE only: no plugin setting, so no Claude Code
|
|
26780
|
+
// version floor. Plugin users set it in the "env" block of ~/.claude/settings.json or export it in the shell
|
|
26781
|
+
// (both reach the plugin's MCP server and its SessionStart hook — measured, Claude Code 2.1.74 and 2.1.302); a custom/Team config sets it
|
|
26782
|
+
// in its server env block. Applied at bridge start: /reload-plugins does NOT restart the server (measured).
|
|
26783
|
+
// The plugin's .mcp.json must NEVER set this key (nor COGENT_WAKE_TOOLS): a plugin env-block key wins over the
|
|
26784
|
+
// shell and settings.json, so any value there would silently block the user's opt-in (measured).
|
|
26785
|
+
//
|
|
26786
|
+
// FUTURE /config option (operator, 2026-10-16: may become the only switch; NOT shipped now). It would be a
|
|
26787
|
+
// plugin.json "userConfig": { "read_only_wake": { "type": "boolean", ... } } mapped by plugin/.mcp.json
|
|
26788
|
+
// "COGENT_READ_ONLY_WAKE": "${user_config.read_only_wake}" — Claude Code passes the STRING "true"/"false"
|
|
26789
|
+
// (measured, 2.1.301). userConfig needs Claude Code >= 2.1.75: 2.1.74 and older reject the manifest
|
|
26790
|
+
// ('Unrecognized key: "userConfig"') and load NONE of the plugin (measured 2026-10-16). Adding that env line
|
|
26791
|
+
// also ends the env-variable path for plugin users (the block wins), so the two cannot ship side by side, and
|
|
26792
|
+
// the guards in src/config.test.ts and cogent-plugin/scripts/sync-from-plugin.test.mjs must change with it.
|
|
26793
|
+
//
|
|
26794
|
+
// Only an explicit affirmative turns it on. Anything else is OFF and never an error — "", "false", a typo, or
|
|
26795
|
+
// an unsubstituted "${user_config.read_only_wake}": a config error here would stop the bridge, which is worse
|
|
26796
|
+
// than a wake that behaves as it did before 3.25.9. The plugin's SessionStart notice reads it with the same rule
|
|
26797
|
+
// (readOnlyWakeFromEnv in plugin/hooks/check-on-stop.lib.mjs, parity-tested).
|
|
26798
|
+
COGENT_READ_ONLY_WAKE: import_zod2.z.string().optional().transform((v) => v !== void 0 && ["true", "1", "yes", "on"].includes(v.trim().toLowerCase())),
|
|
26695
26799
|
// Auto-recovery for a blocked Codex sandbox. On a host where the OS sandbox
|
|
26696
26800
|
// can't initialize (unprivileged user namespaces blocked — Ubuntu 24.04's
|
|
26697
26801
|
// apparmor_restrict_unprivileged_userns, containers, hardened kernels),
|
|
@@ -27352,14 +27456,21 @@ async function execClaude(sessionId, message, cwd, timeoutMs) {
|
|
|
27352
27456
|
// as a raw JSON blob. Flag drift is guarded by claude-preflight.ts.
|
|
27353
27457
|
"--output-format",
|
|
27354
27458
|
"json",
|
|
27459
|
+
// COG-86: an ALLOW list, never a deny list. --disallowedTools Edit,Write,NotebookEdit,Bash was measured to leak
|
|
27460
|
+
// (the Monitor tool, loaded through ToolSearch, ran a shell and wrote a file) and would miss every tool a later
|
|
27461
|
+
// claude adds. --tools is variadic, so it must be followed by another option, never by the message. A claude
|
|
27462
|
+
// too old to know --tools exits "unknown option" — the wake fails, it never runs with every tool.
|
|
27463
|
+
// COG-88: opt-in (COGENT_READ_ONLY_WAKE=true or COGENT_WAKE_TOOLS=read in the env); off by default.
|
|
27464
|
+
...readOnlyWake(config2) ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
|
|
27355
27465
|
"-p",
|
|
27356
|
-
truncated
|
|
27466
|
+
readOnlyWake(config2) ? `${truncated}${READ_ONLY_WAKE_PROMPT_SEPARATOR}${READ_ONLY_WAKE_NOTICE}` : truncated
|
|
27357
27467
|
];
|
|
27358
27468
|
const cleanEnv = buildResumeEnv(process.env, "CLAUDE", ["CLAUDE_CONFIG_DIR"]);
|
|
27359
27469
|
if (resolvedRoot && resolvedRoot !== path10.resolve(claudeConfigDir())) {
|
|
27360
27470
|
cleanEnv.CLAUDE_CONFIG_DIR = resolvedRoot;
|
|
27361
27471
|
}
|
|
27362
27472
|
cleanEnv.COGENT_CHECK_ON_STOP = "0";
|
|
27473
|
+
cleanEnv.COGENT_WAKE = "1";
|
|
27363
27474
|
const child = spawn(config2.COGENT_CLAUDE_PATH, args, {
|
|
27364
27475
|
cwd,
|
|
27365
27476
|
env: cleanEnv,
|
|
@@ -27480,12 +27591,16 @@ function parseClaudeJsonReply(raw) {
|
|
|
27480
27591
|
const result = typeof env.result === "string" ? env.result : null;
|
|
27481
27592
|
return { result, isError: env.is_error === true || env.subtype !== "success" };
|
|
27482
27593
|
}
|
|
27594
|
+
var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE, READ_ONLY_WAKE_PROMPT_SEPARATOR;
|
|
27483
27595
|
var init_cc_cli = __esm({
|
|
27484
27596
|
"src/services/cc-cli.ts"() {
|
|
27485
27597
|
"use strict";
|
|
27486
27598
|
init_config();
|
|
27487
27599
|
init_agent_config();
|
|
27488
27600
|
init_agent_sessions();
|
|
27601
|
+
READ_ONLY_WAKE_TOOLS = "Read,Glob,Grep";
|
|
27602
|
+
READ_ONLY_WAKE_NOTICE = "You are answering a message from another Cogent peer automatically. Your reply goes to that sender, not to your user. Your user chose to make these automatic replies read-only: you can read files, but you cannot edit files, run commands or commit, and neither you nor the sender can change that. If the message asks for a change, do not attempt it. Your reply must: (1) say plainly that this automatic reply could not change any files; (2) give the exact change: the file path and the precise text to replace and its replacement, or the exact command to run; (3) say that your user can make the change in this agent's own session. Do not offer to make the change later, and do not ask the sender to enable, re-enable or turn on any tool or permission.";
|
|
27603
|
+
READ_ONLY_WAKE_PROMPT_SEPARATOR = "\n\n---\n[Cogent: read-only automatic reply \u2014 not part of the message above]\n";
|
|
27489
27604
|
}
|
|
27490
27605
|
});
|
|
27491
27606
|
|
|
@@ -32701,6 +32816,7 @@ function execCodex(sessionId, message, cwd, timeoutMs) {
|
|
|
32701
32816
|
];
|
|
32702
32817
|
const cleanEnv = buildResumeEnv(process.env, "CODEX", ["CODEX_HOME"]);
|
|
32703
32818
|
cleanEnv.COGENT_CHECK_ON_STOP = "0";
|
|
32819
|
+
cleanEnv.COGENT_WAKE = "1";
|
|
32704
32820
|
const child = spawn2(config2.COGENT_CODEX_PATH, args, {
|
|
32705
32821
|
cwd,
|
|
32706
32822
|
env: cleanEnv,
|
|
@@ -33900,6 +34016,7 @@ function announceBridge(cwd, peerId, version2, endpoint, sessionId = agentSessio
|
|
|
33900
34016
|
cwd: path19.resolve(cwd),
|
|
33901
34017
|
parentPid: process.ppid,
|
|
33902
34018
|
...sessionId ? { sessionId } : {},
|
|
34019
|
+
platform: process.env.COGENT_PLATFORM || "cc",
|
|
33903
34020
|
// 🔴 A headless bridge never had a client to lose (review r7 FIX2-2, SEC2-2): when its launching
|
|
33904
34021
|
// shell exited, the changed parent read as "client gone" and any bridge that started took its name.
|
|
33905
34022
|
...startedWithoutClient() ? { noClient: true } : {}
|
|
@@ -35407,7 +35524,13 @@ function mayBeOwnPreSessionBridge(holder, ownSession) {
|
|
|
35407
35524
|
const [maj, min, pat] = [Number(v[1]), Number(v[2]), Number(v[3])];
|
|
35408
35525
|
return maj < 3 || maj === 3 && (min < 25 || min === 25 && pat < 7);
|
|
35409
35526
|
}
|
|
35410
|
-
function heldNameAdvice(peerId) {
|
|
35527
|
+
function heldNameAdvice(peerId, holder, own) {
|
|
35528
|
+
const otherSession = !!(holder?.sessionId && own?.sessionId && holder.sessionId !== own.sessionId);
|
|
35529
|
+
const otherPlatform = !!(holder?.platform && own?.platform && holder.platform !== own.platform);
|
|
35530
|
+
if (otherSession || otherPlatform) {
|
|
35531
|
+
const who = [holder?.platform && `a ${holder.platform} agent`, holder?.sessionId && `session ${holder.sessionId}`].filter(Boolean).join(", ");
|
|
35532
|
+
return `'${peerId}' is held by ANOTHER agent's live bridge in this folder (${who}). It keeps the name while that agent runs, so waiting will not free it. Do not take the name and do not rename: tell the user which agent holds '${peerId}' and ask them to have THAT agent run /cogent:register with ITS OWN peer name (which releases '${peerId}'); after that, run /cogent:register again with the SAME peer name '${peerId}'`;
|
|
35533
|
+
}
|
|
35411
35534
|
return `If this agent was just restarted, its previous connection closes within ${HOLDER_LINGER_SECONDS} seconds: wait ${HOLDER_LINGER_SECONDS}+ seconds, then run /cogent:register again with the SAME peer name '${peerId}'. If '${peerId}' is still held after that, another live connection is using it \u2014 another agent, or an old window of this one still open. Do not take the name and do not rename: ask the user`;
|
|
35412
35535
|
}
|
|
35413
35536
|
function anotherAgentSessionAmong(others, ownSession) {
|
|
@@ -35425,7 +35548,7 @@ function otherBridgeNotice(others, peerId, verdict, ownSession) {
|
|
|
35425
35548
|
}
|
|
35426
35549
|
return `${who} is registered as '${peerId}' too. NOBODY answers for '${peerId}' in this channel now: the bridge that started last (pid ${verdict.owner?.pid ?? "unknown"}) is the one that wakes its agent, but in this channel the relay registration now belongs to this bridge, so the relay refuses that bridge's replies here and this bridge does not wake. It stays that way until that bridge is closed or its agent registers again. If that bridge is registered in another channel, it still answers there.`;
|
|
35427
35550
|
});
|
|
35428
|
-
return lines.join(" ") + (anotherAgentSessionAmong(others, ownSession) ? `
|
|
35551
|
+
return lines.join(" ") + (anotherAgentSessionAmong(others, ownSession) ? ` Each agent in this folder keeps its own saved name (COG-80): after a restart it comes back under that name, not the other agent's.` : "");
|
|
35429
35552
|
}
|
|
35430
35553
|
function autoPresenceEnabled(env = process.env) {
|
|
35431
35554
|
const v = env.COGENT_AUTO_PRESENCE;
|
|
@@ -35441,6 +35564,10 @@ async function restorePresence(deps) {
|
|
|
35441
35564
|
return { state: "online", restored: false, detail: "registered by this process" };
|
|
35442
35565
|
}
|
|
35443
35566
|
const creds = await deps.loadCredentials();
|
|
35567
|
+
const skip = creds ? void 0 : deps.restoreSkip?.();
|
|
35568
|
+
if (skip) {
|
|
35569
|
+
return { state: "deferred", restored: false, detail: `nothing restored: ${skip}` };
|
|
35570
|
+
}
|
|
35444
35571
|
if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
|
|
35445
35572
|
return {
|
|
35446
35573
|
state: "deferred",
|
|
@@ -35479,7 +35606,7 @@ async function restorePresence(deps) {
|
|
|
35479
35606
|
const why = {
|
|
35480
35607
|
"same-session": `That bridge serves this same agent session (${session})`,
|
|
35481
35608
|
"no-session": `Neither bridge records an agent session (codex, gemini), so it cannot be told whether that bridge is this agent's previous one or another agent's. ${take}`,
|
|
35482
|
-
"other-session": `That bridge serves another agent session (${servedBy.sessionId}; ours: ${session}).
|
|
35609
|
+
"other-session": `That bridge serves another agent session (${servedBy.sessionId}; ours: ${session}). ` + heldNameAdvice(peerId, { sessionId: servedBy.sessionId }, { sessionId: session }),
|
|
35483
35610
|
unknown: `Its agent session cannot be compared with this one (that bridge: ${servedBy.sessionId ?? "not recorded"}; this bridge: ${session ?? "not known"}): it may be this agent's own other bridge of an older version, or another agent's. ${take}`
|
|
35484
35611
|
};
|
|
35485
35612
|
return {
|
|
@@ -35741,14 +35868,18 @@ var init_heartbeat = __esm({
|
|
|
35741
35868
|
// src/services/claude-preflight.ts
|
|
35742
35869
|
import { execFile as execFile4 } from "node:child_process";
|
|
35743
35870
|
import { promisify as promisify3 } from "node:util";
|
|
35744
|
-
function
|
|
35745
|
-
|
|
35746
|
-
|
|
35871
|
+
function expectedClaudeFlags(readOnly) {
|
|
35872
|
+
return readOnly ? [...EXPECTED_CLAUDE_FLAGS, ...READ_ONLY_WAKE_CLAUDE_FLAGS] : EXPECTED_CLAUDE_FLAGS;
|
|
35873
|
+
}
|
|
35874
|
+
function analyzeClaudeHelp(helpText, readOnly = false) {
|
|
35875
|
+
const expected = expectedClaudeFlags(readOnly);
|
|
35876
|
+
const checked = expected.map((f) => f.flag);
|
|
35877
|
+
const missing = expected.filter(
|
|
35747
35878
|
(f) => !helpText.includes(f.flag)
|
|
35748
35879
|
).map((f) => f.flag);
|
|
35749
35880
|
return { missing, checked };
|
|
35750
35881
|
}
|
|
35751
|
-
async function preflightClaude(claudePath) {
|
|
35882
|
+
async function preflightClaude(claudePath, readOnly = false) {
|
|
35752
35883
|
let helpText;
|
|
35753
35884
|
try {
|
|
35754
35885
|
const { stdout, stderr } = await execFileAsync2(claudePath, ["--help"], {
|
|
@@ -35762,9 +35893,9 @@ ${stderr}`;
|
|
|
35762
35893
|
);
|
|
35763
35894
|
return null;
|
|
35764
35895
|
}
|
|
35765
|
-
const analysis = analyzeClaudeHelp(helpText);
|
|
35896
|
+
const analysis = analyzeClaudeHelp(helpText, readOnly);
|
|
35766
35897
|
if (analysis.missing.length > 0) {
|
|
35767
|
-
const detail =
|
|
35898
|
+
const detail = expectedClaudeFlags(readOnly).filter(
|
|
35768
35899
|
(f) => analysis.missing.includes(f.flag)
|
|
35769
35900
|
).map((f) => `${f.flag} (${f.usedFor})`).join(", ");
|
|
35770
35901
|
logger.error(
|
|
@@ -35777,7 +35908,7 @@ ${stderr}`;
|
|
|
35777
35908
|
}
|
|
35778
35909
|
return analysis;
|
|
35779
35910
|
}
|
|
35780
|
-
var execFileAsync2, EXPECTED_CLAUDE_FLAGS;
|
|
35911
|
+
var execFileAsync2, EXPECTED_CLAUDE_FLAGS, READ_ONLY_WAKE_CLAUDE_FLAGS;
|
|
35781
35912
|
var init_claude_preflight = __esm({
|
|
35782
35913
|
"src/services/claude-preflight.ts"() {
|
|
35783
35914
|
"use strict";
|
|
@@ -35791,6 +35922,10 @@ var init_claude_preflight = __esm({
|
|
|
35791
35922
|
{ flag: "--strict-mcp-config", usedFor: "isolate MCP config of the resumed process" },
|
|
35792
35923
|
{ flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" }
|
|
35793
35924
|
];
|
|
35925
|
+
READ_ONLY_WAKE_CLAUDE_FLAGS = [
|
|
35926
|
+
{ flag: "--tools", usedFor: "read-only wake: Read, Glob and Grep only (COG-86)" },
|
|
35927
|
+
{ flag: "--append-system-prompt", usedFor: "tell a read-only wake it cannot change anything (COG-86)" }
|
|
35928
|
+
];
|
|
35794
35929
|
}
|
|
35795
35930
|
});
|
|
35796
35931
|
|
|
@@ -36019,7 +36154,9 @@ async function restorePeerPresence(attempt = 0) {
|
|
|
36019
36154
|
},
|
|
36020
36155
|
agentSessionId: () => agentSessionId(),
|
|
36021
36156
|
startedByWakeTurn: () => insideCodexWakeTurn(process.pid, ps()),
|
|
36022
|
-
|
|
36157
|
+
// COG-80: the identity THIS agent saved, not whichever agent registered in this folder last.
|
|
36158
|
+
loadCredentials: () => restoreCandidate(),
|
|
36159
|
+
restoreSkip: () => lastRestoreSkip,
|
|
36023
36160
|
cwd: () => process.cwd(),
|
|
36024
36161
|
recheckSeconds: Math.round(standbyRecheckMs() / 1e3),
|
|
36025
36162
|
goOnline: async ({ peerId, endpoint, sessionId, token, cwd, label, peerSecret }) => {
|
|
@@ -36027,7 +36164,7 @@ async function restorePeerPresence(attempt = 0) {
|
|
|
36027
36164
|
await reinitCloudBackend(endpoint, sessionId, token);
|
|
36028
36165
|
const backend = getBackend();
|
|
36029
36166
|
await backend.registerPeer(...presenceRegisterArgs({ peerId, sessionId, cwd, label, peerSecret }));
|
|
36030
|
-
const slot = await
|
|
36167
|
+
const slot = await restoreCandidate();
|
|
36031
36168
|
if (!stillCurrent() || slot?.sessionId !== sessionId || slot?.endpoint !== endpoint) {
|
|
36032
36169
|
throw new Error(PRESENCE_SUPERSEDED);
|
|
36033
36170
|
}
|
|
@@ -36159,9 +36296,20 @@ async function resolvePendingLabel() {
|
|
|
36159
36296
|
logger.warn(`Post-connect label upgrade failed (non-fatal): ${err}`);
|
|
36160
36297
|
}
|
|
36161
36298
|
}
|
|
36299
|
+
async function restoreCandidate() {
|
|
36300
|
+
const choice = chooseRestoreCredentials(
|
|
36301
|
+
await loadCredentials(),
|
|
36302
|
+
await loadPeerRecords(),
|
|
36303
|
+
agentSessionId(),
|
|
36304
|
+
getConfig().COGENT_PLATFORM
|
|
36305
|
+
);
|
|
36306
|
+
lastRestoreSkip = choice.skip;
|
|
36307
|
+
return choice.creds;
|
|
36308
|
+
}
|
|
36162
36309
|
async function reinitCloudBackend(endpoint, sessionId, token) {
|
|
36163
36310
|
const config2 = getConfig();
|
|
36164
36311
|
cloudHttpClient = new HttpClient(endpoint, token);
|
|
36312
|
+
setCurrentCloudConnection({ endpoint, sessionId, token });
|
|
36165
36313
|
const newBackend = new HttpBackend(cloudHttpClient, sessionId);
|
|
36166
36314
|
resetBackend();
|
|
36167
36315
|
initBackend(newBackend);
|
|
@@ -36266,6 +36414,7 @@ async function runStartup() {
|
|
|
36266
36414
|
cloudInbox = new MessageInbox();
|
|
36267
36415
|
bindInboxNotifier();
|
|
36268
36416
|
cloudHttpClient = new HttpClient(effectiveEndpoint, effectiveToken);
|
|
36417
|
+
setCurrentCloudConnection({ endpoint: effectiveEndpoint, sessionId: effectiveSessionId, token: effectiveToken });
|
|
36269
36418
|
cloudWsClient = createCloudWsClient(
|
|
36270
36419
|
effectiveEndpoint,
|
|
36271
36420
|
effectiveSessionId,
|
|
@@ -36297,10 +36446,10 @@ async function runPreflights() {
|
|
|
36297
36446
|
setSandboxOverride("bypass");
|
|
36298
36447
|
}
|
|
36299
36448
|
} else {
|
|
36300
|
-
await preflightClaude(config2.COGENT_CLAUDE_PATH);
|
|
36449
|
+
await preflightClaude(config2.COGENT_CLAUDE_PATH, readOnlyWake(config2));
|
|
36301
36450
|
}
|
|
36302
36451
|
}
|
|
36303
|
-
var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve;
|
|
36452
|
+
var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve, lastRestoreSkip;
|
|
36304
36453
|
var init_startup = __esm({
|
|
36305
36454
|
"src/startup.ts"() {
|
|
36306
36455
|
"use strict";
|
|
@@ -36495,7 +36644,7 @@ function registerRegisterPeerTool(server) {
|
|
|
36495
36644
|
new BridgeError(
|
|
36496
36645
|
"PEER_HELD_IN_FOLDER" /* PEER_HELD_IN_FOLDER */,
|
|
36497
36646
|
`'${peerId}' is held by another live connection in this folder (bridge pid ${holder.pid}, version ${registryText(holder.version)}); this bridge has NOT registered`,
|
|
36498
|
-
heldNameAdvice(peerId)
|
|
36647
|
+
heldNameAdvice(peerId, holder, { sessionId: own, platform: config2.COGENT_PLATFORM })
|
|
36499
36648
|
)
|
|
36500
36649
|
);
|
|
36501
36650
|
}
|
|
@@ -36612,7 +36761,9 @@ function registerRegisterPeerTool(server) {
|
|
|
36612
36761
|
let storedPeerSecret = argPeerSecret;
|
|
36613
36762
|
if (storedPeerSecret === void 0 && isCloudMode()) {
|
|
36614
36763
|
try {
|
|
36615
|
-
|
|
36764
|
+
const own = await loadPeerRecord(peerId);
|
|
36765
|
+
const folder = own ? null : await loadCredentials();
|
|
36766
|
+
storedPeerSecret = own?.peerSecret ?? (!folder?.peerId || folder.peerId === peerId ? folder?.peerSecret : void 0);
|
|
36616
36767
|
} catch {
|
|
36617
36768
|
storedPeerSecret = void 0;
|
|
36618
36769
|
}
|
|
@@ -36637,7 +36788,8 @@ function registerRegisterPeerTool(server) {
|
|
|
36637
36788
|
let recovered;
|
|
36638
36789
|
if (/PEER_NOT_YOURS/.test(msg) && isCloudMode()) {
|
|
36639
36790
|
try {
|
|
36640
|
-
const
|
|
36791
|
+
const folderCreds = await loadCredentials();
|
|
36792
|
+
const creds = await loadPeerRecord(peerId) ?? (!folderCreds?.peerId || folderCreds.peerId === peerId ? folderCreds : null);
|
|
36641
36793
|
if (creds?.ownerToken && creds.ownerToken !== creds.token && creds.endpoint && creds.sessionId) {
|
|
36642
36794
|
logger.warn(
|
|
36643
36795
|
`register_peer: '${peerId}' is owned by an earlier token (its peerSecret was lost by a pre-3.23.5 re-join). Re-proving ownership with the stored owner token.`
|
|
@@ -36659,7 +36811,7 @@ function registerRegisterPeerTool(server) {
|
|
|
36659
36811
|
recovered = await res.json();
|
|
36660
36812
|
const secret = recovered?.peerSecret;
|
|
36661
36813
|
if (secret) {
|
|
36662
|
-
await
|
|
36814
|
+
await savePeerRecord({ ...creds, peerId, peerSecret: secret, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
36663
36815
|
logger.info(`register_peer: ownership reclaimed for '${peerId}'; a fresh peerSecret is stored.`);
|
|
36664
36816
|
}
|
|
36665
36817
|
}
|
|
@@ -36693,13 +36845,24 @@ function registerRegisterPeerTool(server) {
|
|
|
36693
36845
|
try {
|
|
36694
36846
|
const creds = await loadCredentials();
|
|
36695
36847
|
const issuedSecret = peer?.peerSecret;
|
|
36696
|
-
|
|
36697
|
-
|
|
36698
|
-
|
|
36699
|
-
|
|
36700
|
-
|
|
36848
|
+
const platform = getConfig().COGENT_PLATFORM;
|
|
36849
|
+
const agent = { agentSession: (platform === "cc" ? agentSessionId() : void 0) ?? sessionId, platform };
|
|
36850
|
+
if (creds) {
|
|
36851
|
+
const channel = getCurrentCloudConnection() ?? { endpoint: creds.endpoint, sessionId: creds.sessionId, token: creds.token };
|
|
36852
|
+
const mine = peerCredentials(creds, peerId);
|
|
36853
|
+
const sameChannel = !!mine && mine.sessionId === channel.sessionId && mine.endpoint === channel.endpoint;
|
|
36854
|
+
const secret = issuedSecret ?? (sameChannel ? mine.peerSecret : void 0);
|
|
36855
|
+
const ownerToken = sameChannel ? mine.ownerToken ?? (mine.token !== channel.token ? mine.token : void 0) : void 0;
|
|
36856
|
+
const label2 = creds.sessionId === channel.sessionId ? creds.label : sameChannel ? mine.label : void 0;
|
|
36857
|
+
const entry = {
|
|
36858
|
+
...channel,
|
|
36859
|
+
...label2 ? { label: label2 } : {},
|
|
36860
|
+
...agent,
|
|
36861
|
+
...secret ? { peerSecret: secret } : {},
|
|
36862
|
+
...ownerToken ? { ownerToken } : {},
|
|
36701
36863
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
36702
|
-
}
|
|
36864
|
+
};
|
|
36865
|
+
await saveCredentials({ ...entry, peerId, peers: { ...creds.peers ?? {}, [peerId]: entry } });
|
|
36703
36866
|
}
|
|
36704
36867
|
} catch (err) {
|
|
36705
36868
|
logger.debug(
|
|
@@ -37819,7 +37982,8 @@ function registerDeregisterPeerTool(server) {
|
|
|
37819
37982
|
const backend = getBackend();
|
|
37820
37983
|
let ownershipProof;
|
|
37821
37984
|
try {
|
|
37822
|
-
|
|
37985
|
+
const folder = await loadCredentials();
|
|
37986
|
+
ownershipProof = (await loadPeerRecord(peerId))?.peerSecret ?? (!folder?.peerId || folder.peerId === peerId ? folder?.peerSecret : void 0);
|
|
37823
37987
|
} catch {
|
|
37824
37988
|
}
|
|
37825
37989
|
const { removed, mailboxDeprovisioned } = await backend.deregisterPeer(peerId, ownershipProof);
|
|
@@ -37840,6 +38004,7 @@ function registerDeregisterPeerTool(server) {
|
|
|
37840
38004
|
await saveCredentials({ ...rest, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
37841
38005
|
logger.info("cleared stored peerId after deregister", { peerId });
|
|
37842
38006
|
}
|
|
38007
|
+
await deletePeerRecord(peerId);
|
|
37843
38008
|
} catch (err) {
|
|
37844
38009
|
logger.warn("failed to clear stored peerId after deregister", { error: err });
|
|
37845
38010
|
}
|
|
@@ -21,6 +21,16 @@ export function parseScope(csv) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* COG-88: are this agent's Claude wakes read-only? MUST equal src/config.ts readOnlyWake(loadConfig(env)):
|
|
26
|
+
* COGENT_READ_ONLY_WAKE truthy ("true"/"1"/"yes"/"on") OR COGENT_WAKE_TOOLS = "read", each trimmed and
|
|
27
|
+
* case-folded. Parity is tested against the REAL loadConfig (plugin/hooks/record-session.runtime.test.mjs).
|
|
28
|
+
*/
|
|
29
|
+
export function readOnlyWakeFromEnv(env) {
|
|
30
|
+
const v = (k) => String(env[k] ?? "").trim().toLowerCase();
|
|
31
|
+
return ["true", "1", "yes", "on"].includes(v("COGENT_READ_ONLY_WAKE")) || v("COGENT_WAKE_TOOLS") === "read";
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
/** sha256(path.resolve(cwd))[..16] — MUST equal credential-store.defaultCredentialPath's hash. */
|
|
25
35
|
export function credHashForCwd(cwd) {
|
|
26
36
|
return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
|
|
@@ -84,6 +94,27 @@ export function buildAgentSessionRecord(payload, nowIso) {
|
|
|
84
94
|
};
|
|
85
95
|
}
|
|
86
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Which saved identity this hook's agent is (COG-80). MUST follow src/cloud/credential-store.ts
|
|
99
|
+
* chooseRestoreCredentials + peerCredentials. `folder` is the folder file; its `peers` map holds one
|
|
100
|
+
* entry per registered peer. The top-level fields name the last-registered peer and are that peer's
|
|
101
|
+
* newest write. Returns null when the folder's names provably belong to other agents (stay silent
|
|
102
|
+
* rather than act as the other agent in the folder).
|
|
103
|
+
*/
|
|
104
|
+
export function chooseHookCredentials(folder, sessionId, platform) {
|
|
105
|
+
if (!folder) return null;
|
|
106
|
+
const records = Object.entries(folder.peers || {}).map(([peerId, e]) => ({ ...e, peerId }));
|
|
107
|
+
if (records.length === 0) return folder;
|
|
108
|
+
const resolve = (peerId) => (folder.peerId === peerId ? folder : { ...folder.peers[peerId], peerId });
|
|
109
|
+
const mine = sessionId ? records.filter((r) => r.agentSession === sessionId) : [];
|
|
110
|
+
if (mine.length > 0) return resolve(mine[0].peerId);
|
|
111
|
+
const sameKind = records.filter((r) => r.platform === platform);
|
|
112
|
+
if (sameKind.length === 1) return resolve(sameKind[0].peerId);
|
|
113
|
+
if (sameKind.length > 1 && folder.peerId && sameKind.some((r) => r.peerId === folder.peerId)) return folder;
|
|
114
|
+
if (sameKind.length === 0 && folder.peerId && !records.some((r) => r.peerId === folder.peerId)) return folder;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
function isForMe(msg, me, scope) {
|
|
88
119
|
if (msg.toPeerId === me) return scope.directed;
|
|
89
120
|
if (msg.toPeerId === "*") {
|
package/hooks/check-on-stop.mjs
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
activeInFlightIds,
|
|
21
21
|
agentSessionRecordPath,
|
|
22
22
|
buildAgentSessionRecord,
|
|
23
|
+
chooseHookCredentials,
|
|
24
|
+
rootFromTranscriptPath,
|
|
23
25
|
} from "./check-on-stop.lib.mjs";
|
|
24
26
|
|
|
25
27
|
const HTTP_TIMEOUT_MS = 5000; // < the 8s hook timeout; only bites on a degraded relay
|
|
@@ -143,7 +145,9 @@ async function main() {
|
|
|
143
145
|
if (envOff(env.COGENT_CHECK_ON_STOP)) return process.exit(0); // opt-out
|
|
144
146
|
const cwd = input.cwd || process.cwd();
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
// COG-80: this agent's own record — the folder file names whichever agent registered last.
|
|
149
|
+
const creds = chooseHookCredentials(await readJson(credPath(cwd, env)), input.session_id,
|
|
150
|
+
rootFromTranscriptPath(input.transcript_path) ? "cc" : "codex");
|
|
147
151
|
if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
|
|
148
152
|
return process.exit(0); // not registered in cloud mode → nothing to do
|
|
149
153
|
}
|
package/hooks/record-session.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Cogent record-session. Runs on SessionStart as a SEPARATE short-lived process
|
|
3
3
|
// (the host spawns it; it is NOT the bridge). Writes one small file and exits 0.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Silent, with ONE exception (COG-88, below): the once-only wake-permissions notice,
|
|
5
|
+
// printed as {"systemMessage": ...} — valid SessionStart hook output that the host
|
|
6
|
+
// shows to the human and never feeds to the model. It can never block or fail a turn.
|
|
6
7
|
//
|
|
7
8
|
// 🔴 WHY THIS EXISTS. Auto-wake has to find the agent's session transcript, and it
|
|
8
9
|
// used to DERIVE the location:
|
|
@@ -23,10 +24,12 @@
|
|
|
23
24
|
// Node built-ins only (fs, os, path). Byte-identical between plugin/hooks/ and
|
|
24
25
|
// codex-plugin/hooks/ — enforced by scripts/wake-race-test.mjs assertContract().
|
|
25
26
|
import fs from "node:fs/promises";
|
|
27
|
+
import os from "node:os";
|
|
26
28
|
import path from "node:path";
|
|
27
29
|
import {
|
|
28
30
|
agentSessionRecordPath,
|
|
29
31
|
buildAgentSessionRecord,
|
|
32
|
+
readOnlyWakeFromEnv,
|
|
30
33
|
} from "./check-on-stop.lib.mjs";
|
|
31
34
|
|
|
32
35
|
async function readStdin() {
|
|
@@ -55,8 +58,70 @@ export async function persistRecord(record) {
|
|
|
55
58
|
return target;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
// ── COG-88: tell the human ONCE what a Cogent wake may do in this folder ──────────────
|
|
62
|
+
// 3.25.9 made wakes read-only by default; 3.25.10 turned that back into an opt-in: the environment variable
|
|
63
|
+
// COGENT_READ_ONLY_WAKE=true (or COGENT_WAKE_TOOLS=read). Nobody reads a changelog, so the person at the
|
|
64
|
+
// terminal is told once, at a session start, what wakes can do and how to switch it.
|
|
65
|
+
//
|
|
66
|
+
// The mode is read from this hook's own env with the bridge's rule (readOnlyWakeFromEnv = readOnlyWake()).
|
|
67
|
+
// Measured on Claude Code 2.1.302 and 2.1.74 (throwaway CLAUDE_CONFIG_DIR, probe plugin): a settings.json "env" value and
|
|
68
|
+
// a shell export reach BOTH this hook and the plugin's MCP server, so the hook sees what the bridge sees.
|
|
69
|
+
// Also measured: after a settings.json change, /reload-plugins does NOT restart the plugin's MCP server (it
|
|
70
|
+
// keeps the old value), so the notice says "restart Claude Code".
|
|
71
|
+
//
|
|
72
|
+
// Shown ONLY when all hold:
|
|
73
|
+
// CLAUDECODE set — Claude Code. Codex never sets it (and shows systemMessage only
|
|
74
|
+
// after the first prompt).
|
|
75
|
+
// CLAUDE_CODE_SESSION_ATTENDED === "1" — a human is at the TUI ("0" in headless -p).
|
|
76
|
+
// COGENT_WAKE !== "1" — not inside a wake: the resumed claude runs this
|
|
77
|
+
// hook too (source "resume"), with the wake's env,
|
|
78
|
+
// and a notice there is swallowed but would still
|
|
79
|
+
// burn the once-only marker.
|
|
80
|
+
// the marker does not exist — once per notice version.
|
|
81
|
+
// Measured: Claude Code 2.1.302 gives a TUI SessionStart hook both variables; 2.1.74 gives it NEITHER (clean env, real
|
|
82
|
+
// TUI), so on 2.1.74 the notice is never shown. Deliberate: nothing there tells a TUI from headless -p, and a
|
|
83
|
+
// notice nobody sees would burn the marker. The opt-in itself still works on 2.1.74 (env reaches the bridge).
|
|
84
|
+
// The marker is created with "wx" BEFORE printing, so two sessions starting together
|
|
85
|
+
// cannot both show it, and it is never written unless the notice is printed.
|
|
86
|
+
export const WAKE_NOTICE_MARKER = "wake-tools-notice-v1";
|
|
87
|
+
|
|
88
|
+
export function wakeNoticeText(env) {
|
|
89
|
+
// Claude Code reads <CLAUDE_CONFIG_DIR>/settings.json when that variable is set; naming ~/.claude there
|
|
90
|
+
// would send the user to a file with no effect.
|
|
91
|
+
const dir = (env.CLAUDE_CONFIG_DIR || "").trim();
|
|
92
|
+
const settings = dir ? path.join(dir, "settings.json") : "~/.claude/settings.json";
|
|
93
|
+
if (readOnlyWakeFromEnv(env)) {
|
|
94
|
+
const names = ["COGENT_READ_ONLY_WAKE", "COGENT_WAKE_TOOLS"].filter((k) => readOnlyWakeFromEnv({ [k]: env[k] }));
|
|
95
|
+
const setting = names.join(" and ");
|
|
96
|
+
return (
|
|
97
|
+
`Cogent: wakes are read-only (${setting} ${names.length > 1 ? "are" : "is"} set) — when a Cogent message ` +
|
|
98
|
+
"wakes this agent, the wake can read files and answer, but cannot edit files or run commands. To let wakes " +
|
|
99
|
+
`edit again, remove ${setting} from the "env" block of ${settings} (or from your shell), then restart ` +
|
|
100
|
+
"Claude Code. (Shown once.)"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
"Cogent: when a Cogent message wakes this agent, the wake can edit files and run commands in this folder. " +
|
|
105
|
+
`To make wakes read-only, add "COGENT_READ_ONLY_WAKE": "true" to the "env" block of ${settings} ` +
|
|
106
|
+
"(or export it in your shell), then restart Claude Code. (Shown once.)"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function maybeShowWakeNotice(env) {
|
|
111
|
+
if (!env.CLAUDECODE || env.CLAUDE_CODE_SESSION_ATTENDED !== "1" || env.COGENT_WAKE === "1") return;
|
|
112
|
+
const dir = env.CLAUDE_PLUGIN_DATA || path.join(os.homedir(), ".cogent", "notices");
|
|
113
|
+
await fs.mkdir(dir, { recursive: true });
|
|
114
|
+
// EEXIST (already shown) and every other failure throw → swallowed by the caller: silent.
|
|
115
|
+
await fs.writeFile(path.join(dir, WAKE_NOTICE_MARKER), `${new Date().toISOString()}\n`, { flag: "wx" });
|
|
116
|
+
const out = JSON.stringify({ systemMessage: wakeNoticeText(env) });
|
|
117
|
+
// Wait for the write: a pipe can be asynchronous (macOS), and exit() would cut it off.
|
|
118
|
+
await new Promise((resolve) => process.stdout.write(out, () => resolve()));
|
|
119
|
+
}
|
|
120
|
+
|
|
58
121
|
async function main() {
|
|
59
122
|
const input = await readStdin();
|
|
123
|
+
// Independent of the record below, and never allowed to break it (or the session start).
|
|
124
|
+
await maybeShowWakeNotice(process.env).catch(() => {});
|
|
60
125
|
const record = buildAgentSessionRecord(input, new Date().toISOString());
|
|
61
126
|
// No transcript_path in this payload → nothing authoritative to record. The
|
|
62
127
|
// resolver's discovery layer still covers this case; staying silent is correct.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essentialai/cogent-plugin",
|
|
3
|
-
"version": "3.25.
|
|
3
|
+
"version": "3.25.10",
|
|
4
4
|
"description": "Cogent — Claude Code plugin (skills + slash-commands + MCP server) for the cross-agent comms fabric.",
|
|
5
5
|
"author": { "name": "Essential AI Solutions Ltd.", "url": "https://essentialai.uk" },
|
|
6
6
|
"homepage": "https://cogent.tools",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [".claude-plugin", "commands", "skills", ".mcp.json", "hooks", "bridge", "README.md", "LICENSE"],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"sync": "node scripts/sync-from-plugin.mjs",
|
|
13
|
+
"test": "node --test scripts/sync-from-plugin.test.mjs",
|
|
13
14
|
"prepack": "node scripts/sync-from-plugin.mjs"
|
|
14
15
|
}
|
|
15
16
|
}
|