@essentialai/cogent-plugin 3.20.7 → 3.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bridge/cogent-bridge.mjs +415 -183
- package/hooks/check-on-stop.lib.mjs +59 -0
- package/hooks/check-on-stop.mjs +29 -0
- package/hooks/hooks.json +12 -0
- package/hooks/record-session.mjs +72 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// defaultCredentialPath. Kept separate from the runtime (check-on-stop.mjs) so it is
|
|
4
4
|
// unit-tested (plugin/hooks/check-on-stop.lib.test.mjs, gate-counted via vitest include).
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
|
|
8
9
|
/** MUST equal src/services/auto-relay.ts:629 — human-origin broadcasts only. */
|
|
@@ -25,6 +26,64 @@ export function credHashForCwd(cwd) {
|
|
|
25
26
|
return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* MUST equal src/services/agent-sessions.ts rootFromTranscriptPath.
|
|
31
|
+
*
|
|
32
|
+
* Recovers the agent's config ROOT from an absolute transcript path by STRUCTURE
|
|
33
|
+
* (`<root>/projects/<encoded-cwd>/<id>.jsonl`) rather than by matching a known
|
|
34
|
+
* prefix — which is the whole point: a root nobody has ever seen still resolves.
|
|
35
|
+
*/
|
|
36
|
+
export function rootFromTranscriptPath(transcriptPath) {
|
|
37
|
+
if (!transcriptPath) return null;
|
|
38
|
+
const parts = path.resolve(transcriptPath).split(path.sep);
|
|
39
|
+
const i = parts.lastIndexOf("projects");
|
|
40
|
+
if (i <= 0) return null;
|
|
41
|
+
return parts.slice(0, i).join(path.sep) || path.sep;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** MUST equal src/services/agent-sessions.ts agentSessionPath. */
|
|
45
|
+
export function agentSessionRecordPath(cwd, platform) {
|
|
46
|
+
return path.join(
|
|
47
|
+
os.homedir(),
|
|
48
|
+
".cogent",
|
|
49
|
+
"agent-sessions",
|
|
50
|
+
`${credHashForCwd(cwd)}.${platform}.json`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Turn a hook payload into a learned session record — the AUTHORITATIVE answer to
|
|
56
|
+
* "where does this agent's transcript live". Pure; the caller does the write.
|
|
57
|
+
*
|
|
58
|
+
* The host hands every hook `cwd`, `session_id` and `transcript_path`. Persisting
|
|
59
|
+
* that trio removes both guesses the wake resolver used to make (which config root,
|
|
60
|
+
* and how the cwd was encoded into a directory name), so a relocated or otherwise
|
|
61
|
+
* unusual agent installation resolves without any configuration.
|
|
62
|
+
*
|
|
63
|
+
* `platform` is inferred STRUCTURALLY so this file can stay byte-identical across
|
|
64
|
+
* the Claude and Codex plugins: only Claude Code lays transcripts out under a
|
|
65
|
+
* `projects/` segment, so a recoverable root means Claude Code.
|
|
66
|
+
*
|
|
67
|
+
* Returns null when the payload lacks any of the three fields (some hosts/events
|
|
68
|
+
* omit them) — a partial record is worse than none, because it would look
|
|
69
|
+
* authoritative while pointing nowhere.
|
|
70
|
+
*/
|
|
71
|
+
export function buildAgentSessionRecord(payload, nowIso) {
|
|
72
|
+
const cwd = payload?.cwd;
|
|
73
|
+
const sessionId = payload?.session_id;
|
|
74
|
+
const transcriptPath = payload?.transcript_path;
|
|
75
|
+
if (!cwd || !sessionId || !transcriptPath) return null;
|
|
76
|
+
const root = rootFromTranscriptPath(transcriptPath);
|
|
77
|
+
return {
|
|
78
|
+
cwd: path.resolve(cwd),
|
|
79
|
+
sessionId: String(sessionId),
|
|
80
|
+
transcriptPath: path.resolve(transcriptPath),
|
|
81
|
+
...(root ? { root } : {}),
|
|
82
|
+
platform: root ? "cc" : "codex",
|
|
83
|
+
updatedAt: nowIso,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
28
87
|
function isForMe(msg, me, scope) {
|
|
29
88
|
if (msg.toPeerId === me) return scope.directed;
|
|
30
89
|
if (msg.toPeerId === "*") {
|
package/hooks/check-on-stop.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
buildBlockReason,
|
|
19
19
|
credHashForCwd,
|
|
20
20
|
activeInFlightIds,
|
|
21
|
+
agentSessionRecordPath,
|
|
22
|
+
buildAgentSessionRecord,
|
|
21
23
|
} from "./check-on-stop.lib.mjs";
|
|
22
24
|
|
|
23
25
|
const HTTP_TIMEOUT_MS = 5000; // < the 8s hook timeout; only bites on a degraded relay
|
|
@@ -104,9 +106,36 @@ async function peerIdFromState(env, cwd) {
|
|
|
104
106
|
return byCwd ? byCwd.peerId : undefined;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Refresh the learned session record from this payload — the authoritative answer
|
|
111
|
+
* to "where does this agent's transcript live" (see record-session.mjs for the
|
|
112
|
+
* full rationale). Done here as well as on SessionStart because this hook already
|
|
113
|
+
* receives the payload on every turn, so the record stays correct for free even
|
|
114
|
+
* if the session was started before the plugin was installed. Best-effort and
|
|
115
|
+
* completely silent: it must never affect whether this hook blocks.
|
|
116
|
+
*/
|
|
117
|
+
async function refreshSessionRecord(input) {
|
|
118
|
+
try {
|
|
119
|
+
const record = buildAgentSessionRecord(input, new Date().toISOString());
|
|
120
|
+
if (!record) return;
|
|
121
|
+
const target = agentSessionRecordPath(record.cwd, record.platform);
|
|
122
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
123
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
124
|
+
await fsp.writeFile(tmp, JSON.stringify(record, null, 2), "utf-8");
|
|
125
|
+
await fsp.rename(tmp, target);
|
|
126
|
+
} catch {
|
|
127
|
+
// learning is an optimisation, never a reason to change this hook's outcome
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
107
131
|
async function main() {
|
|
108
132
|
const input = await readStdin();
|
|
109
133
|
|
|
134
|
+
// Record BEFORE the loop guard: a continuation turn still carries a valid
|
|
135
|
+
// transcript path, and skipping it would lose the refresh on exactly the turns
|
|
136
|
+
// where auto-wake is most active.
|
|
137
|
+
await refreshSessionRecord(input);
|
|
138
|
+
|
|
110
139
|
// Loop guard: if we already caused this continuation, never block again.
|
|
111
140
|
if (input && input.stop_hook_active === true) return process.exit(0);
|
|
112
141
|
|
package/hooks/hooks.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cogent record-session. Runs on SessionStart as a SEPARATE short-lived process
|
|
3
|
+
// (the host spawns it; it is NOT the bridge). Writes one small file and exits 0.
|
|
4
|
+
// ALWAYS silent — it prints nothing on success or failure, so it can never block,
|
|
5
|
+
// delay, or annotate a turn.
|
|
6
|
+
//
|
|
7
|
+
// 🔴 WHY THIS EXISTS. Auto-wake has to find the agent's session transcript, and it
|
|
8
|
+
// used to DERIVE the location:
|
|
9
|
+
//
|
|
10
|
+
// <config root> / projects / <cwd with non-alphanumerics turned into dashes>
|
|
11
|
+
//
|
|
12
|
+
// Both halves are guesses. The root was `CLAUDE_CONFIG_DIR` or a hardcoded
|
|
13
|
+
// `~/.claude`; the cwd half assumed the exact canonical spelling of the directory.
|
|
14
|
+
// Users relocate agent state (small system disks, synced or encrypted homes,
|
|
15
|
+
// multi-account machines, containers) and the env var does not always reach the
|
|
16
|
+
// process that needs it. Either guess being wrong produced the same dead end:
|
|
17
|
+
// "no candidate sessions found for cwd", and a refused wake.
|
|
18
|
+
//
|
|
19
|
+
// The host already knows the answer and hands it to every hook: `cwd`,
|
|
20
|
+
// `session_id` and `transcript_path`. Recording that trio turns discovery from
|
|
21
|
+
// inference into a lookup, and works with ANY custom location without config.
|
|
22
|
+
//
|
|
23
|
+
// Node built-ins only (fs, os, path). Byte-identical between plugin/hooks/ and
|
|
24
|
+
// codex-plugin/hooks/ — enforced by scripts/wake-race-test.mjs assertContract().
|
|
25
|
+
import fs from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import {
|
|
28
|
+
agentSessionRecordPath,
|
|
29
|
+
buildAgentSessionRecord,
|
|
30
|
+
} from "./check-on-stop.lib.mjs";
|
|
31
|
+
|
|
32
|
+
async function readStdin() {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
35
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
36
|
+
if (!raw) return {};
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Atomic temp+rename: the bridge may read this file at any moment, and a
|
|
46
|
+
* half-written record would look authoritative while pointing nowhere. The
|
|
47
|
+
* temp name carries the pid so two concurrent hooks cannot collide.
|
|
48
|
+
*/
|
|
49
|
+
export async function persistRecord(record) {
|
|
50
|
+
const target = agentSessionRecordPath(record.cwd, record.platform);
|
|
51
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
52
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
53
|
+
await fs.writeFile(tmp, JSON.stringify(record, null, 2), "utf-8");
|
|
54
|
+
await fs.rename(tmp, target);
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const input = await readStdin();
|
|
60
|
+
const record = buildAgentSessionRecord(input, new Date().toISOString());
|
|
61
|
+
// No transcript_path in this payload → nothing authoritative to record. The
|
|
62
|
+
// resolver's discovery layer still covers this case; staying silent is correct.
|
|
63
|
+
if (!record) return;
|
|
64
|
+
await persistRecord(record);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Best-effort by construction: recording is an optimisation for a LATER wake, so
|
|
68
|
+
// nothing here may ever fail a turn. Any error exits 0 silently.
|
|
69
|
+
main().then(
|
|
70
|
+
() => process.exit(0),
|
|
71
|
+
() => process.exit(0),
|
|
72
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essentialai/cogent-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.1",
|
|
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",
|