klypix-mcp 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/bin/klypix-conformance.mjs +189 -0
- package/bin/klypix-install.mjs +8 -4
- package/bin/klypix-mcp.mjs +9 -1
- package/package.json +3 -2
- package/src/agent-presence.mjs +33 -0
- package/src/brain-doctor.mjs +3 -3
- package/src/codex-brain-hook.mjs +77 -1
- package/src/codex-hooks.mjs +13 -3
- package/src/mcp-presence.mjs +47 -1
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ Claude Code keeps its live hooks for auto-brief + auto-capture. Codex gets nativ
|
|
|
20
20
|
tools, **automatic live-session presence**, and approval-free **smart task synchronization**
|
|
21
21
|
from the authorized MCP connection itself. `brain_sync` is a bounded **Context Gateway**:
|
|
22
22
|
one call returns task-relevant brain memory, meaningful active-task peers, structured
|
|
23
|
-
exact-file conflicts, one-time notes, and automatic late-arrival overlap alerts.
|
|
23
|
+
exact-file conflicts, one-time notes, and automatic late-arrival overlap alerts. The
|
|
24
|
+
earlier session gets a best-effort live MCP logging notification within seconds, while
|
|
25
|
+
the same alert remains guaranteed on its next KLYPIX action if its host hides logs. Idle MCP
|
|
24
26
|
connections stay counted for diagnostics but are hidden from the task-peer list. MCP
|
|
25
27
|
server instructions plus a managed Codex `AGENTS.md` block teach Codex to call it at task
|
|
26
28
|
start, when scope changes, and on completion—no Codex hook approval required. Run inside a
|
|
@@ -35,8 +37,19 @@ npx klypix-mcp install --codex-hooks
|
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
Codex owns this native layer's security decision and asks the user to review/trust the
|
|
38
|
-
hooks on surfaces that expose hook management.
|
|
39
|
-
|
|
40
|
+
hooks on surfaces that expose hook management. Once trusted, the enhanced layer
|
|
41
|
+
automatically injects bounded task-ranked brain memory on every prompt and warns before
|
|
42
|
+
an exact overlapping file edit. `brain_doctor` reports it separately as off,
|
|
43
|
+
execution-unverified, or active; the approval-free smart layer remains active either way.
|
|
44
|
+
|
|
45
|
+
Prove the exact installed runtime with two isolated real MCP clients:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx klypix-mcp conformance
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The test touches only a temporary fixture. It verifies tool discovery, task memory,
|
|
52
|
+
truthful peers, blocking overlap detection, proactive logging, and guaranteed in-band delivery.
|
|
40
53
|
|
|
41
54
|
Give a project a brain by dropping a `brain.klypix` in it — the
|
|
42
55
|
[KLYPIX app](https://klypix.com) does it in one click (*Save canvas as project brain*),
|
|
@@ -91,7 +104,7 @@ Any MCP client gets the tools and connection-lifecycle presence without host hoo
|
|
|
91
104
|
If the vault contains `brain.klypix`, that MCP connection also appears as an active
|
|
92
105
|
session. `brain_sync` adds compact task memory, expected-file coordination, task-only peer
|
|
93
106
|
reporting, structured exact-overlap warnings, automatic conflict alerts, timing receipts,
|
|
94
|
-
and one-time peer-note delivery. Then ask your
|
|
107
|
+
best-effort proactive notifications, and guaranteed one-time peer-note delivery. Then ask your
|
|
95
108
|
agent things like *"what did we decide about auth?"*, *"challenge this: let's switch
|
|
96
109
|
to polling"*, or *"turn these notes into a board."*
|
|
97
110
|
|
|
@@ -132,7 +145,7 @@ it, enrich it with intent/files, and remove only their own channel. The TTL cove
|
|
|
132
145
|
|
|
133
146
|
Future hosts get baseline support merely by connecting the MCP server. A deeper adapter
|
|
134
147
|
can import `klypix-mcp/presence` and map lifecycle events to `upsertSession`,
|
|
135
|
-
`removeSession`, and `receiveMessages`. The shared contract accepts `id`, `client`,
|
|
148
|
+
`removeSession`, `peekMessages`, and `receiveMessages`. The shared contract accepts `id`, `client`,
|
|
136
149
|
`surface`, `model`, `branch`, `intent`, touched `files`, and adapter `channel`;
|
|
137
150
|
host-specific transcript parsing stays outside the protocol.
|
|
138
151
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Self-contained KLYPIX MCP conformance test.
|
|
3
|
+
//
|
|
4
|
+
// Launches two real stdio clients against this exact installed server build,
|
|
5
|
+
// using a temporary brain + isolated presence registry. It verifies the
|
|
6
|
+
// Context Gateway, task memory, truthful task counts, exact overlap detection,
|
|
7
|
+
// proactive MCP logging, and guaranteed next-action delivery without touching
|
|
8
|
+
// the user's project or durable brain.
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
15
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
16
|
+
import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
17
|
+
import { buildKlypixMap } from '../src/klypix-core.mjs';
|
|
18
|
+
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const jsonMode = process.argv.includes('--json');
|
|
21
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
const PKG_VERSION = (() => {
|
|
23
|
+
try { return createRequire(import.meta.url)('../package.json').version; }
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
const stamp = JSON.parse(fs.readFileSync(path.join(here, '.brain-version.json'), 'utf8'));
|
|
27
|
+
return stamp.brainVersion || stamp.appVersion || 'deployed';
|
|
28
|
+
} catch { return 'deployed'; }
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
const serverPath = [
|
|
32
|
+
path.join(here, 'klypix-mcp.mjs'),
|
|
33
|
+
path.join(here, 'klypix-mcp-server.mjs'),
|
|
34
|
+
].find((candidate) => fs.existsSync(candidate));
|
|
35
|
+
|
|
36
|
+
if (!serverPath) {
|
|
37
|
+
console.error('KLYPIX conformance failed: the MCP server executable is missing beside this command.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'klypix-conformance-'));
|
|
42
|
+
const vault = path.join(tempRoot, 'project');
|
|
43
|
+
const isolatedHome = path.join(tempRoot, 'home');
|
|
44
|
+
fs.mkdirSync(vault, { recursive: true });
|
|
45
|
+
fs.mkdirSync(isolatedHome, { recursive: true });
|
|
46
|
+
fs.writeFileSync(path.join(vault, 'brain.klypix'), await buildKlypixMap({
|
|
47
|
+
title: 'KLYPIX conformance brain',
|
|
48
|
+
kind: 'brain',
|
|
49
|
+
areas: [{
|
|
50
|
+
title: 'Brain',
|
|
51
|
+
cards: [{
|
|
52
|
+
text: 'Codex Context Gateway automatically coordinates active tasks and exact file overlaps across MCP sessions.',
|
|
53
|
+
}],
|
|
54
|
+
}],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const baseEnv = Object.fromEntries(
|
|
58
|
+
Object.entries(process.env).filter(([, value]) => typeof value === 'string'),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
async function connect(name, sessionId) {
|
|
62
|
+
const logs = [];
|
|
63
|
+
const transport = new StdioClientTransport({
|
|
64
|
+
command: process.execPath,
|
|
65
|
+
args: [serverPath, '--vault', vault],
|
|
66
|
+
env: {
|
|
67
|
+
...baseEnv,
|
|
68
|
+
USERPROFILE: isolatedHome,
|
|
69
|
+
HOME: isolatedHome,
|
|
70
|
+
KLYPIX_SESSION_ID: sessionId,
|
|
71
|
+
KLYPIX_MCP_INBOX_POLL_MS: '250',
|
|
72
|
+
KLYPIX_RERANK: '0',
|
|
73
|
+
},
|
|
74
|
+
stderr: 'pipe',
|
|
75
|
+
});
|
|
76
|
+
const client = new Client({ name, version: '1.0.0' });
|
|
77
|
+
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
78
|
+
logs.push(String(notification?.params?.data || ''));
|
|
79
|
+
});
|
|
80
|
+
await client.connect(transport);
|
|
81
|
+
await client.setLoggingLevel('info');
|
|
82
|
+
return { client, transport, logs };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const checks = {};
|
|
86
|
+
const metrics = {};
|
|
87
|
+
let a;
|
|
88
|
+
let b;
|
|
89
|
+
try {
|
|
90
|
+
a = await connect('codex-klypix-conformance-a', 'klypix-conformance-a');
|
|
91
|
+
b = await connect('codex-klypix-conformance-b', 'klypix-conformance-b');
|
|
92
|
+
const tools = await a.client.listTools();
|
|
93
|
+
checks.brainSyncDiscoverable = tools.tools?.some((tool) => tool.name === 'brain_sync') === true;
|
|
94
|
+
|
|
95
|
+
const aStart = await a.client.callTool({
|
|
96
|
+
name: 'brain_sync',
|
|
97
|
+
arguments: {
|
|
98
|
+
phase: 'start',
|
|
99
|
+
intent: 'Validate automatic Codex Context Gateway coordination',
|
|
100
|
+
files: ['src/conformance-overlap.ts'],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const bStart = await b.client.callTool({
|
|
104
|
+
name: 'brain_sync',
|
|
105
|
+
arguments: {
|
|
106
|
+
phase: 'start',
|
|
107
|
+
intent: 'Validate proactive overlap delivery from a second session',
|
|
108
|
+
files: ['src/conformance-overlap.ts'],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const aStructured = aStart.structuredContent || {};
|
|
112
|
+
const bStructured = bStart.structuredContent || {};
|
|
113
|
+
checks.taskMemory = Array.isArray(aStructured.context?.hits) && aStructured.context.hits.length > 0;
|
|
114
|
+
checks.truthfulTaskCount = bStructured.counts?.activeTasks === 2
|
|
115
|
+
&& bStructured.peers?.length === 1;
|
|
116
|
+
checks.exactBlockingOverlap = bStructured.conflicts?.length === 1
|
|
117
|
+
&& bStructured.conflicts[0]?.kind === 'exact-file'
|
|
118
|
+
&& bStructured.conflicts[0]?.severity === 'blocking';
|
|
119
|
+
checks.alertQueued = bStructured.alertsQueued?.length === 1;
|
|
120
|
+
metrics.firstClientMs = aStructured.timingMs?.total ?? null;
|
|
121
|
+
metrics.secondClientMs = bStructured.timingMs?.total ?? null;
|
|
122
|
+
|
|
123
|
+
const pushDeadline = Date.now() + 4_000;
|
|
124
|
+
while (Date.now() < pushDeadline
|
|
125
|
+
&& !a.logs.some((line) => line.includes('Automatic KLYPIX overlap alert'))) {
|
|
126
|
+
await wait(100);
|
|
127
|
+
}
|
|
128
|
+
checks.proactiveLogging = a.logs.some((line) =>
|
|
129
|
+
line.includes('Automatic KLYPIX overlap alert'));
|
|
130
|
+
metrics.firstClientLogCount = a.logs.length;
|
|
131
|
+
if (!checks.proactiveLogging && a.logs.length) metrics.firstClientLogs = a.logs.slice(-4);
|
|
132
|
+
|
|
133
|
+
const aCheckpoint = await a.client.callTool({
|
|
134
|
+
name: 'brain_sync',
|
|
135
|
+
arguments: {
|
|
136
|
+
phase: 'checkpoint',
|
|
137
|
+
intent: 'Validate automatic Codex Context Gateway coordination',
|
|
138
|
+
files: ['src/conformance-overlap.ts'],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const checkpointStructured = aCheckpoint.structuredContent || {};
|
|
142
|
+
checks.guaranteedInBandDelivery = checkpointStructured.messages?.some((message) =>
|
|
143
|
+
String(message.text).includes('Automatic KLYPIX overlap alert')) === true;
|
|
144
|
+
|
|
145
|
+
await Promise.all([
|
|
146
|
+
a.client.callTool({ name: 'brain_sync', arguments: { phase: 'complete' } }),
|
|
147
|
+
b.client.callTool({ name: 'brain_sync', arguments: { phase: 'complete' } }),
|
|
148
|
+
]);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
checks.runtime = false;
|
|
151
|
+
checks.error = error?.message || String(error);
|
|
152
|
+
} finally {
|
|
153
|
+
await Promise.allSettled([a?.client?.close(), b?.client?.close()]);
|
|
154
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const required = [
|
|
158
|
+
'brainSyncDiscoverable',
|
|
159
|
+
'taskMemory',
|
|
160
|
+
'truthfulTaskCount',
|
|
161
|
+
'exactBlockingOverlap',
|
|
162
|
+
'alertQueued',
|
|
163
|
+
'proactiveLogging',
|
|
164
|
+
'guaranteedInBandDelivery',
|
|
165
|
+
];
|
|
166
|
+
const ok = required.every((name) => checks[name] === true);
|
|
167
|
+
const result = {
|
|
168
|
+
ok,
|
|
169
|
+
packageVersion: PKG_VERSION,
|
|
170
|
+
checks,
|
|
171
|
+
metrics,
|
|
172
|
+
contract: {
|
|
173
|
+
proactive: 'best-effort MCP logging notification',
|
|
174
|
+
guaranteed: 'same alert on the next KLYPIX action',
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (jsonMode) {
|
|
179
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`KLYPIX MCP conformance v${PKG_VERSION}: ${ok ? 'PASS' : 'FAIL'}`);
|
|
182
|
+
for (const name of required) {
|
|
183
|
+
console.log(` ${checks[name] ? '✓' : '✗'} ${name}`);
|
|
184
|
+
}
|
|
185
|
+
if (checks.error) console.log(` error: ${checks.error}`);
|
|
186
|
+
console.log(` memory/coordination: ${metrics.firstClientMs ?? '?'}ms / ${metrics.secondClientMs ?? '?'}ms`);
|
|
187
|
+
console.log(' proactive notifications are best-effort; next-action delivery is guaranteed.');
|
|
188
|
+
}
|
|
189
|
+
process.exit(ok ? 0 : 1);
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -276,7 +276,11 @@ try {
|
|
|
276
276
|
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
277
277
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
278
278
|
}
|
|
279
|
-
for (const [src, dst] of [
|
|
279
|
+
for (const [src, dst] of [
|
|
280
|
+
['klypix-mcp.mjs', 'klypix-mcp-server.mjs'],
|
|
281
|
+
['klypix-a2a.mjs', 'klypix-a2a-server.mjs'],
|
|
282
|
+
['klypix-conformance.mjs', 'klypix-conformance.mjs'],
|
|
283
|
+
]) {
|
|
280
284
|
const s = path.join(BIN, src); if (exists(s)) staged.push({ dst, content: flatten(fs.readFileSync(s, 'utf8')) });
|
|
281
285
|
}
|
|
282
286
|
for (const st of staged) fs.writeFileSync(path.join(BRAIN_DIR, st.dst + '.klypix-new'), st.content);
|
|
@@ -344,10 +348,10 @@ try {
|
|
|
344
348
|
else console.error(`⚠ readiness: ${notWired.length} hook(s) did NOT take (${notWired.join(', ')}) — the brain will read but not capture/sync. Re-run \`npx klypix-mcp install --force\` or check ${SETTINGS}.`);
|
|
345
349
|
console.log(`✓ MCP server runs from the local bundle (node ${fwd(path.join(BRAIN_DIR, 'klypix-mcp-server.mjs'))}) — no npx cache, works offline, always the installed version.`);
|
|
346
350
|
if (migrated) console.log(`✓ migrated ${migrated.file} klypix-canvas server: ${migrated.from} → ${migrated.to} (backup: .mcp.json.klypix-bak). Reconnect (/mcp) or restart to pick it up.`);
|
|
347
|
-
console.log(' Claude Code keeps its existing auto-brief/capture hooks; Codex gets the brain_sync Context Gateway (compact task memory + clean peers +
|
|
348
|
-
console.log('
|
|
351
|
+
console.log(' Claude Code keeps its existing auto-brief/capture hooks; Codex gets the brain_sync Context Gateway (compact task memory + clean peers + proactive/guaranteed conflict alerts) with no hook trust prompt.');
|
|
352
|
+
console.log(' Enhanced Codex auto-context + pre-edit overlap guard: re-run with `--codex-hooks`, then approve/review KLYPIX once in a Codex surface that supports hook trust.');
|
|
349
353
|
console.log(' ⚠ Open sessions keep their OLD server until relaunched: fully quit & reopen the app (a session resume / new chat does NOT respawn the MCP server). `brain_doctor`\'s RUNNING line confirms when you\'re current.');
|
|
350
|
-
console.log(' Verify anytime: `npx klypix-mcp doctor
|
|
354
|
+
console.log(' Verify anytime: `npx klypix-mcp doctor`; prove two-client behavior with `npx klypix-mcp conformance`.');
|
|
351
355
|
} catch (e) {
|
|
352
356
|
if (gotLock) releaseLock();
|
|
353
357
|
console.error(`✗ install failed: ${e?.message || e}`);
|
package/bin/klypix-mcp.mjs
CHANGED
|
@@ -57,6 +57,11 @@ if (process.argv[2] === 'link') { await import('./klypix-link.mjs'); process.exi
|
|
|
57
57
|
// projection in sync? One verdict, one reconcile block. Exits 1 on drift (CI gate).
|
|
58
58
|
if (process.argv[2] === 'doctor') { await import('./klypix-doctor.mjs'); process.exit(0); }
|
|
59
59
|
|
|
60
|
+
// `npx klypix-mcp conformance` — launch two real, isolated MCP clients against
|
|
61
|
+
// this exact installed server and verify task memory, truthful peers, blocking
|
|
62
|
+
// overlap detection, proactive logging, and guaranteed next-action delivery.
|
|
63
|
+
if (process.argv[2] === 'conformance') { await import('./klypix-conformance.mjs'); process.exit(0); }
|
|
64
|
+
|
|
60
65
|
// `npx klypix-mcp garden-code` — the HUMAN half of the garden approval gate.
|
|
61
66
|
// brain_garden's apply requires an 8-char code derived from the exact dormant
|
|
62
67
|
// candidate set + day; the agent is deliberately never shown it. The human runs
|
|
@@ -106,7 +111,10 @@ const vaultArgIdx = process.argv.indexOf('--vault');
|
|
|
106
111
|
const VAULT = resolveVault(vaultArgIdx >= 0 ? process.argv[vaultArgIdx + 1] : undefined);
|
|
107
112
|
const server = new McpServer(
|
|
108
113
|
{ name: 'klypix-canvas', version: PKG_VERSION },
|
|
109
|
-
{
|
|
114
|
+
{
|
|
115
|
+
instructions: KLYPIX_MCP_INSTRUCTIONS,
|
|
116
|
+
capabilities: { logging: {} },
|
|
117
|
+
},
|
|
110
118
|
);
|
|
111
119
|
const mcpPresence = createMcpPresence({ server, initialVault: VAULT });
|
|
112
120
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "Every project gets a brain — one open .klypix file your AI agents read, write, and argue from, over MCP. Works with Claude, Codex, Cursor, Cline, any model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
28
|
"klypix-mcp": "bin/klypix-mcp.mjs",
|
|
29
|
+
"klypix-conformance": "bin/klypix-conformance.mjs",
|
|
29
30
|
"klypix-link": "bin/klypix-link.mjs",
|
|
30
31
|
"klypix-doctor": "bin/klypix-doctor.mjs",
|
|
31
32
|
"klypix-a2a": "bin/klypix-a2a.mjs",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"node": ">=18"
|
|
56
57
|
},
|
|
57
58
|
"scripts": {
|
|
58
|
-
"test": "node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs"
|
|
59
|
+
"test": "node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
package/src/agent-presence.mjs
CHANGED
|
@@ -292,6 +292,39 @@ export function receiveMessages({
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// Non-destructive inbox preview for MCP logging notifications. Unlike
|
|
296
|
+
// receiveMessages(), this NEVER appends the session id to message.seen, so a
|
|
297
|
+
// host that ignores notifications cannot make a coordination warning vanish.
|
|
298
|
+
// The next KLYPIX tool result or lifecycle hook still receives + acknowledges
|
|
299
|
+
// the same message through the guaranteed in-band path.
|
|
300
|
+
export function peekMessages({
|
|
301
|
+
brainPath,
|
|
302
|
+
sessionId,
|
|
303
|
+
ignoreTexts = [],
|
|
304
|
+
home,
|
|
305
|
+
now = Date.now(),
|
|
306
|
+
}) {
|
|
307
|
+
if (!brainPath || !sessionId) return [];
|
|
308
|
+
const data = readLane(laneFileFor(brainPath, home));
|
|
309
|
+
const sessions = pruneSessions(data.sessions, now);
|
|
310
|
+
const messages = pruneMessages(data.messages, now);
|
|
311
|
+
const me = sessions.find((session) => session.id === sessionId);
|
|
312
|
+
const ignored = new Set(ignoreTexts.map((value) =>
|
|
313
|
+
String(value || '').replace(/\s+/g, ' ').trim().toLowerCase()));
|
|
314
|
+
const shown = [];
|
|
315
|
+
const seenText = new Set();
|
|
316
|
+
for (const message of messages) {
|
|
317
|
+
if (message.from === sessionId
|
|
318
|
+
|| (Array.isArray(message.seen) && message.seen.includes(sessionId))
|
|
319
|
+
|| !messageTargetsSession(message, me, sessionId)) continue;
|
|
320
|
+
const key = String(message.text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
321
|
+
if (!key || ignored.has(key) || seenText.has(key)) continue;
|
|
322
|
+
seenText.add(key);
|
|
323
|
+
shown.push(message);
|
|
324
|
+
}
|
|
325
|
+
return shown.slice(0, 6);
|
|
326
|
+
}
|
|
327
|
+
|
|
295
328
|
// Queue a one-time coordination message directly on the shared presence lane.
|
|
296
329
|
// `dedupeKey` makes machine-generated alerts (for example, exact file overlap)
|
|
297
330
|
// idempotent without weakening deliberate brain_message notes.
|
package/src/brain-doctor.mjs
CHANGED
|
@@ -290,7 +290,7 @@ export function render(r, opts = {}) {
|
|
|
290
290
|
else L.push(`${hmark} ${c.bold}CLAUDE${c.rst} existing 4-hook capture path intact: ${r.hooks.wired.join(', ')}`);
|
|
291
291
|
const chmark = r.layers.codexHooks === 'warning' ? warn : ok;
|
|
292
292
|
const smart = r.codexSmart?.globalInstructions
|
|
293
|
-
? 'approval-free Context Gateway active (task memory + clean peers +
|
|
293
|
+
? 'approval-free Context Gateway active (task memory + clean peers + proactive/guaranteed alerts)'
|
|
294
294
|
: 'approval-free brain_sync Context Gateway available through MCP';
|
|
295
295
|
if (r.codexHooks.error) {
|
|
296
296
|
L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart}; optional native-hook config unreadable: ${c.yel}${r.codexHooks.error}${c.rst}`);
|
|
@@ -298,9 +298,9 @@ export function render(r, opts = {}) {
|
|
|
298
298
|
L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native hooks off ${c.dim}(optional)${c.rst}`);
|
|
299
299
|
} else if (r.codexHooks.executionStatus !== 'observed') {
|
|
300
300
|
L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · ${c.yel}native hooks configured but execution not verified${c.rst}`);
|
|
301
|
-
L.push(` ${c.dim}Context Gateway memory/coordination already works.
|
|
301
|
+
L.push(` ${c.dim}Context Gateway memory/coordination already works. Once trusted, native hooks auto-inject task memory and warn before exact overlapping edits.${c.rst}`);
|
|
302
302
|
} else {
|
|
303
|
-
L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native
|
|
303
|
+
L.push(`${chmark} ${c.bold}CODEX${c.rst} automatic MCP presence + ${smart} · native auto-context + pre-edit guard active ${c.dim}(last observed ${r.codexHooks.lastExecutedAt})${c.rst}`);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
// TOOLS
|
package/src/codex-brain-hook.mjs
CHANGED
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
findProjectBrain,
|
|
9
9
|
formatPresenceMessage,
|
|
10
10
|
formatReceivedMessages,
|
|
11
|
+
postPresenceMessage,
|
|
11
12
|
receiveMessages,
|
|
12
13
|
removeSession,
|
|
13
14
|
upsertSession,
|
|
14
15
|
} from './agent-presence.mjs';
|
|
15
16
|
import { recordCodexHookExecution } from './codex-hooks.mjs';
|
|
17
|
+
import { opBrainTaskContext } from './klypix-core.mjs';
|
|
18
|
+
import { findPresenceConflicts } from './mcp-presence.mjs';
|
|
16
19
|
|
|
17
20
|
function readInput() {
|
|
18
21
|
try {
|
|
@@ -84,6 +87,53 @@ function emitSystemMessage(parts) {
|
|
|
84
87
|
process.stdout.write(JSON.stringify({ continue: true, systemMessage }));
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
function formatConflictWarning(conflicts, event) {
|
|
91
|
+
if (!conflicts.length) return '';
|
|
92
|
+
const moment = event === 'PreToolUse' ? 'before this edit runs' : 'after this file operation';
|
|
93
|
+
return [
|
|
94
|
+
`KLYPIX BLOCKING exact-file overlap detected ${moment}:`,
|
|
95
|
+
...conflicts.map((peer) =>
|
|
96
|
+
`- ${String(peer.id).slice(0, 12)}${peer.intent ? ` "${String(peer.intent).slice(0, 90)}"` : ''}: ${peer.files.join(', ')}`),
|
|
97
|
+
'Coordinate file ownership now. Do not continue overlapping edits until one session yields or the scopes are separated.',
|
|
98
|
+
].join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function queueConflictAlerts({ brainPath, sessionId, intent, conflicts, turnId }) {
|
|
102
|
+
const queued = [];
|
|
103
|
+
for (const peer of conflicts) {
|
|
104
|
+
const filesKey = peer.files.map((file) => String(file).toLowerCase()).sort().join('|');
|
|
105
|
+
const result = postPresenceMessage({
|
|
106
|
+
brainPath,
|
|
107
|
+
from: sessionId,
|
|
108
|
+
to: peer.id,
|
|
109
|
+
text: `Automatic KLYPIX pre-edit overlap alert: ${String(sessionId).slice(0, 12)}`
|
|
110
|
+
+ `${intent ? ` is working on "${String(intent).slice(0, 110)}"` : ' is editing'}`
|
|
111
|
+
+ ` and reports the same file(s): ${peer.files.join(', ')}. Coordinate ownership now.`,
|
|
112
|
+
dedupeKey: `hook-overlap|${sessionId}|${peer.id}|${turnId || 'turn'}|${filesKey}`,
|
|
113
|
+
});
|
|
114
|
+
if (result.posted) queued.push(peer.id);
|
|
115
|
+
}
|
|
116
|
+
return queued;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function compactTaskContext(projectDir, prompt, files) {
|
|
120
|
+
if (!prompt) return '';
|
|
121
|
+
try {
|
|
122
|
+
const result = await opBrainTaskContext({
|
|
123
|
+
vault: projectDir,
|
|
124
|
+
intent: String(prompt).slice(0, 2000),
|
|
125
|
+
files,
|
|
126
|
+
k: 4,
|
|
127
|
+
budgetChars: 1800,
|
|
128
|
+
});
|
|
129
|
+
return result?.context
|
|
130
|
+
? result.blocks?.filter((block) => block.kind === 'text').map((block) => block.text).filter(Boolean).join('\n\n') || ''
|
|
131
|
+
: '';
|
|
132
|
+
} catch {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
87
137
|
async function main() {
|
|
88
138
|
const input = readInput();
|
|
89
139
|
const event = String(input.hook_event_name || input.hookEventName || '');
|
|
@@ -100,7 +150,9 @@ async function main() {
|
|
|
100
150
|
|
|
101
151
|
const projectDir = path.dirname(brainPath);
|
|
102
152
|
const prompt = input.prompt || input.user_prompt || input.userPrompt;
|
|
103
|
-
const files = event === '
|
|
153
|
+
const files = event === 'PreToolUse' || event === 'PostToolUse'
|
|
154
|
+
? touchedFiles(input, projectDir)
|
|
155
|
+
: (event === 'UserPromptSubmit' ? [] : undefined);
|
|
104
156
|
const sessions = upsertSession({
|
|
105
157
|
brainPath,
|
|
106
158
|
id: sessionId,
|
|
@@ -111,6 +163,7 @@ async function main() {
|
|
|
111
163
|
branch: gitBranch(cwd),
|
|
112
164
|
intent: prompt === undefined ? undefined : prompt,
|
|
113
165
|
files,
|
|
166
|
+
replaceFiles: event === 'UserPromptSubmit',
|
|
114
167
|
event,
|
|
115
168
|
channel: 'lifecycle',
|
|
116
169
|
cwd,
|
|
@@ -118,6 +171,19 @@ async function main() {
|
|
|
118
171
|
|
|
119
172
|
const ignored = event === 'PostToolUse' ? ownBrainMessageText(input) : [];
|
|
120
173
|
const messages = receiveMessages({ brainPath, sessionId, ignoreTexts: ignored });
|
|
174
|
+
const conflicts = (event === 'PreToolUse' || event === 'PostToolUse')
|
|
175
|
+
? findPresenceConflicts(sessions, sessionId)
|
|
176
|
+
: [];
|
|
177
|
+
if (conflicts.length) {
|
|
178
|
+
const me = sessions.find((session) => session.id === sessionId);
|
|
179
|
+
queueConflictAlerts({
|
|
180
|
+
brainPath,
|
|
181
|
+
sessionId,
|
|
182
|
+
intent: me?.intent || '',
|
|
183
|
+
conflicts,
|
|
184
|
+
turnId: input.turn_id || input.turnId || '',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
121
187
|
if (event === 'SessionStart') {
|
|
122
188
|
emitSystemMessage([
|
|
123
189
|
formatPresenceMessage(sessions, sessionId, { includeSolo: true }),
|
|
@@ -126,12 +192,22 @@ async function main() {
|
|
|
126
192
|
return;
|
|
127
193
|
}
|
|
128
194
|
if (event === 'UserPromptSubmit') {
|
|
195
|
+
const me = sessions.find((session) => session.id === sessionId);
|
|
196
|
+
const context = await compactTaskContext(projectDir, prompt, me?.files || []);
|
|
129
197
|
emitSystemMessage([
|
|
198
|
+
context,
|
|
130
199
|
formatPresenceMessage(sessions, sessionId),
|
|
131
200
|
formatReceivedMessages(messages),
|
|
132
201
|
]);
|
|
133
202
|
return;
|
|
134
203
|
}
|
|
204
|
+
if (event === 'PreToolUse' || event === 'PostToolUse') {
|
|
205
|
+
emitSystemMessage([
|
|
206
|
+
formatConflictWarning(conflicts, event),
|
|
207
|
+
formatReceivedMessages(messages),
|
|
208
|
+
]);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
135
211
|
if (messages.length) emitSystemMessage([formatReceivedMessages(messages)]);
|
|
136
212
|
}
|
|
137
213
|
|
package/src/codex-hooks.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import crypto from 'crypto';
|
|
|
10
10
|
export const CODEX_PRESENCE_EVENTS = [
|
|
11
11
|
'SessionStart',
|
|
12
12
|
'UserPromptSubmit',
|
|
13
|
+
'PreToolUse',
|
|
13
14
|
'Stop',
|
|
14
15
|
'PostToolUse',
|
|
15
16
|
'SessionEnd',
|
|
@@ -29,10 +30,19 @@ export function resolveCodexHooksPath(home = os.homedir()) {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function codexPresenceGroups(command) {
|
|
32
|
-
const handler = (timeout = 5) => ({
|
|
33
|
+
const handler = (timeout = 5, statusMessage) => ({
|
|
34
|
+
type: 'command',
|
|
35
|
+
command,
|
|
36
|
+
timeout,
|
|
37
|
+
...(statusMessage ? { statusMessage } : {}),
|
|
38
|
+
});
|
|
33
39
|
return {
|
|
34
|
-
SessionStart: [{ hooks: [handler()] }],
|
|
35
|
-
UserPromptSubmit: [{ hooks: [handler()] }],
|
|
40
|
+
SessionStart: [{ hooks: [handler(5, 'Loading KLYPIX project awareness')] }],
|
|
41
|
+
UserPromptSubmit: [{ hooks: [handler(5, 'Syncing KLYPIX task context')] }],
|
|
42
|
+
PreToolUse: [{
|
|
43
|
+
matcher: 'apply_patch|Edit|Write',
|
|
44
|
+
hooks: [handler(5, 'Checking KLYPIX file ownership')],
|
|
45
|
+
}],
|
|
36
46
|
Stop: [{ hooks: [handler()] }],
|
|
37
47
|
PostToolUse: [{ hooks: [handler()] }],
|
|
38
48
|
SessionEnd: [{ hooks: [handler(3)] }],
|
package/src/mcp-presence.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
findProjectBrain,
|
|
13
13
|
formatPresenceMessage,
|
|
14
14
|
formatReceivedMessages,
|
|
15
|
+
peekMessages,
|
|
15
16
|
postPresenceMessage,
|
|
16
17
|
receiveMessages,
|
|
17
18
|
removeSession,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from './agent-presence.mjs';
|
|
20
21
|
|
|
21
22
|
export const MCP_HEARTBEAT_MS = 60_000;
|
|
23
|
+
export const MCP_INBOX_POLL_MS = 3_000;
|
|
22
24
|
|
|
23
25
|
// Standard MCP server instructions are the approval-free awareness path. Hosts
|
|
24
26
|
// that surface InitializeResult.instructions to the model learn the same
|
|
@@ -185,19 +187,25 @@ export function createMcpPresence({
|
|
|
185
187
|
env = process.env,
|
|
186
188
|
home,
|
|
187
189
|
heartbeatMs = MCP_HEARTBEAT_MS,
|
|
190
|
+
inboxPollMs = MCP_INBOX_POLL_MS,
|
|
188
191
|
now = () => Date.now(),
|
|
189
192
|
setIntervalFn = setInterval,
|
|
190
193
|
clearIntervalFn = clearInterval,
|
|
191
194
|
} = {}) {
|
|
192
195
|
const sessionId = resolveMcpSessionId({ env });
|
|
196
|
+
const effectiveInboxPollMs = Number(env?.KLYPIX_MCP_INBOX_POLL_MS)
|
|
197
|
+
|| Number(inboxPollMs)
|
|
198
|
+
|| MCP_INBOX_POLL_MS;
|
|
193
199
|
let vault = path.resolve(initialVault || process.cwd());
|
|
194
200
|
let brainPath = null;
|
|
195
201
|
let timer = null;
|
|
202
|
+
let inboxTimer = null;
|
|
196
203
|
let started = false;
|
|
197
204
|
let lastPeers = '';
|
|
198
205
|
let taskStartedAt = 0;
|
|
199
206
|
const sentTexts = new Set();
|
|
200
207
|
const notifiedConflicts = new Set();
|
|
208
|
+
const announcedMessageIds = new Set();
|
|
201
209
|
|
|
202
210
|
const clientInfo = () => {
|
|
203
211
|
let version = {};
|
|
@@ -209,12 +217,37 @@ export function createMcpPresence({
|
|
|
209
217
|
};
|
|
210
218
|
|
|
211
219
|
const sendNotice = (message) => {
|
|
212
|
-
if (!message || typeof server?.sendLoggingMessage !== 'function') return;
|
|
220
|
+
if (!message || typeof server?.sendLoggingMessage !== 'function') return false;
|
|
213
221
|
Promise.resolve(server.sendLoggingMessage({
|
|
214
222
|
level: 'info',
|
|
215
223
|
logger: 'klypix-presence',
|
|
216
224
|
data: message,
|
|
217
225
|
})).catch(() => {});
|
|
226
|
+
return true;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Best-effort proactive path: MCP logging notifications can surface an
|
|
230
|
+
// overlap while the older session is still working. This deliberately PEEKS
|
|
231
|
+
// instead of acknowledging. Hosts are allowed to hide logging notifications,
|
|
232
|
+
// so the same warning remains unread and is guaranteed to arrive in-band on
|
|
233
|
+
// the next KLYPIX tool result / brain_sync call.
|
|
234
|
+
const pollInbox = () => {
|
|
235
|
+
if (!brainPath) return [];
|
|
236
|
+
const pending = peekMessages({
|
|
237
|
+
brainPath,
|
|
238
|
+
sessionId,
|
|
239
|
+
ignoreTexts: [...sentTexts],
|
|
240
|
+
home,
|
|
241
|
+
now: now(),
|
|
242
|
+
}).filter((message) => !announcedMessageIds.has(message.id));
|
|
243
|
+
if (!pending.length) return [];
|
|
244
|
+
if (sendNotice(formatReceivedMessages(pending, now()))) {
|
|
245
|
+
for (const message of pending) announcedMessageIds.add(message.id);
|
|
246
|
+
while (announcedMessageIds.size > 100) {
|
|
247
|
+
announcedMessageIds.delete(announcedMessageIds.values().next().value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return pending;
|
|
218
251
|
};
|
|
219
252
|
|
|
220
253
|
const touch = ({
|
|
@@ -271,7 +304,9 @@ export function createMcpPresence({
|
|
|
271
304
|
|
|
272
305
|
const stop = () => {
|
|
273
306
|
if (timer) clearIntervalFn(timer);
|
|
307
|
+
if (inboxTimer) clearIntervalFn(inboxTimer);
|
|
274
308
|
timer = null;
|
|
309
|
+
inboxTimer = null;
|
|
275
310
|
if (brainPath) removeSession({
|
|
276
311
|
brainPath,
|
|
277
312
|
id: sessionId,
|
|
@@ -282,6 +317,7 @@ export function createMcpPresence({
|
|
|
282
317
|
brainPath = null;
|
|
283
318
|
started = false;
|
|
284
319
|
lastPeers = '';
|
|
320
|
+
announcedMessageIds.clear();
|
|
285
321
|
};
|
|
286
322
|
|
|
287
323
|
const start = (nextVault = vault, details = {}) => {
|
|
@@ -302,7 +338,12 @@ export function createMcpPresence({
|
|
|
302
338
|
replaceFiles: details.replaceFiles === true,
|
|
303
339
|
});
|
|
304
340
|
timer = setIntervalFn(() => touch(), Math.max(5_000, Number(heartbeatMs) || MCP_HEARTBEAT_MS));
|
|
341
|
+
inboxTimer = setIntervalFn(
|
|
342
|
+
() => pollInbox(),
|
|
343
|
+
Math.max(250, effectiveInboxPollMs),
|
|
344
|
+
);
|
|
305
345
|
timer?.unref?.();
|
|
346
|
+
inboxTimer?.unref?.();
|
|
306
347
|
return first;
|
|
307
348
|
};
|
|
308
349
|
|
|
@@ -398,6 +439,10 @@ export function createMcpPresence({
|
|
|
398
439
|
ts: message.ts,
|
|
399
440
|
})),
|
|
400
441
|
alertsQueued,
|
|
442
|
+
delivery: {
|
|
443
|
+
proactive: 'mcp-logging-best-effort',
|
|
444
|
+
guaranteed: 'next-klypix-action',
|
|
445
|
+
},
|
|
401
446
|
timingMs: { coordination: durationMs },
|
|
402
447
|
};
|
|
403
448
|
const text = [
|
|
@@ -429,6 +474,7 @@ export function createMcpPresence({
|
|
|
429
474
|
start,
|
|
430
475
|
stop,
|
|
431
476
|
touch,
|
|
477
|
+
pollInbox,
|
|
432
478
|
sync,
|
|
433
479
|
noteSent,
|
|
434
480
|
decorateToolResult,
|