klypix-mcp 1.34.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 +60 -22
- package/bin/klypix-conformance.mjs +189 -0
- package/bin/klypix-install.mjs +89 -22
- package/bin/klypix-link.mjs +11 -3
- package/bin/klypix-mcp.mjs +90 -8
- package/package.json +5 -3
- package/src/agent-presence.mjs +135 -6
- package/src/agent-rules.mjs +50 -6
- package/src/brain-doctor.mjs +33 -14
- package/src/codex-brain-hook.mjs +81 -2
- package/src/codex-hooks.mjs +80 -4
- package/src/global-brain-hook.mjs +6 -3
- package/src/klypix-core.mjs +96 -1
- package/src/mcp-presence.mjs +484 -0
package/README.md
CHANGED
|
@@ -10,18 +10,46 @@ You've seen the setup: point an AI at a folder of notes, watch the graph fill up
|
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
|
-
**Claude Code + Codex
|
|
13
|
+
**Claude Code + Codex:**
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npx klypix-mcp install
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Claude Code keeps its live hooks for auto-brief + auto-capture. Codex gets
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
Claude Code keeps its live hooks for auto-brief + auto-capture. Codex gets native MCP
|
|
20
|
+
tools, **automatic live-session presence**, and approval-free **smart task synchronization**
|
|
21
|
+
from the authorized MCP connection itself. `brain_sync` is a bounded **Context Gateway**:
|
|
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. 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
|
|
26
|
+
connections stay counted for diagnostics but are hidden from the task-peer list. MCP
|
|
27
|
+
server instructions plus a managed Codex `AGENTS.md` block teach Codex to call it at task
|
|
28
|
+
start, when scope changes, and on completion—no Codex hook approval required. Run inside a
|
|
29
|
+
project containing `brain.klypix`, or run `npx klypix-mcp link` in each brain project.
|
|
30
|
+
The installer removes the obsolete global KLYPIX `--vault "."` table that could resolve
|
|
31
|
+
outside the active project; every unrelated Codex setting and MCP server is preserved.
|
|
32
|
+
|
|
33
|
+
Optional native lifecycle capture for mechanical prompt/tool/file events:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx klypix-mcp install --codex-hooks
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Codex owns this native layer's security decision and asks the user to review/trust the
|
|
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.
|
|
25
53
|
|
|
26
54
|
Give a project a brain by dropping a `brain.klypix` in it — the
|
|
27
55
|
[KLYPIX app](https://klypix.com) does it in one click (*Save canvas as project brain*),
|
|
@@ -41,7 +69,7 @@ managed files without changing them with `npx klypix-mcp link --check`.
|
|
|
41
69
|
|
|
42
70
|
The difference from a folder of notes is not the shape — it's that this memory is a **mechanism, not a filing convention**:
|
|
43
71
|
|
|
44
|
-
- **It briefs
|
|
72
|
+
- **It briefs each task, not just each session.** `brain_sync` ranks a bounded memory capsule from the actual task intent and expected files. The full generated brief remains available for broad history/status work, while the always-loaded `AGENTS.md` fallback stays compact. Measured on our own brain: **73% of past decisions recovered with one search round (55% brief-only) vs 0% cold** (n=20).
|
|
45
73
|
- **It argues back.** `brain_challenge`: propose a decision and the brain answers with receipts — *"you reversed this on June 12; here's the correction — captured by a different agent, coordinate before overriding."* Deterministic evidence only (correction-cues, opposite-polarity), never mere topical similarity. A memory that can't disagree with you is flattery.
|
|
46
74
|
- **It knows when it's stale.** Decision cards can anchor to the exact code they were decided against (git blob OID). When that code moves on, the card raises its hand in the next brief. Their notes rot silently; ours confess.
|
|
47
75
|
- **It answers from the past.** `brain_ask` with `as_of: 2026-03-01` answers what the project believed *then* — corrections from the future never leak backwards.
|
|
@@ -59,7 +87,8 @@ And it's not a cage: everything **exports to Markdown and JSON Canvas** in one c
|
|
|
59
87
|
|
|
60
88
|
## Setup for plain MCP clients
|
|
61
89
|
|
|
62
|
-
Any MCP client gets the tools without
|
|
90
|
+
Any MCP client gets the tools and connection-lifecycle presence without host hooks. For
|
|
91
|
+
**Claude Desktop**, in `claude_desktop_config.json`:
|
|
63
92
|
|
|
64
93
|
```json
|
|
65
94
|
{
|
|
@@ -72,9 +101,14 @@ Any MCP client gets the tools without the hooks. For **Claude Desktop**, in `cla
|
|
|
72
101
|
}
|
|
73
102
|
```
|
|
74
103
|
|
|
75
|
-
|
|
104
|
+
If the vault contains `brain.klypix`, that MCP connection also appears as an active
|
|
105
|
+
session. `brain_sync` adds compact task memory, expected-file coordination, task-only peer
|
|
106
|
+
reporting, structured exact-overlap warnings, automatic conflict alerts, timing receipts,
|
|
107
|
+
best-effort proactive notifications, and guaranteed one-time peer-note delivery. Then ask your
|
|
108
|
+
agent things like *"what did we decide about auth?"*, *"challenge this: let's switch
|
|
109
|
+
to polling"*, or *"turn these notes into a board."*
|
|
76
110
|
|
|
77
|
-
## The
|
|
111
|
+
## The 18 verbs
|
|
78
112
|
|
|
79
113
|
| Tool | What it does |
|
|
80
114
|
|---|---|
|
|
@@ -83,9 +117,11 @@ Then ask your agent things like *"what did we decide about auth?"*, *"challenge
|
|
|
83
117
|
| `brain_note` | Capture with the full lifecycle — supersede / ✓ resolve / ~ update / 🛠 skill / closes: |
|
|
84
118
|
| `brain_reconcile` | Find stale-vs-correction pairs + unrecorded migrations |
|
|
85
119
|
| `brain_insights` | Hubs, orphaned decisions, stale questions, area sizes |
|
|
120
|
+
| `brain_lens` | Machine-readable freshness, provenance, activity, timeline, orrery, and unresolved views |
|
|
86
121
|
| `brain_garden` | Maintenance pass over the brain |
|
|
87
|
-
| `brain_doctor` | Self-diagnosis: version, host adapters, active
|
|
122
|
+
| `brain_doctor` | Self-diagnosis: version, core/enhanced host adapters, active sessions, projection drift |
|
|
88
123
|
| `brain_message` | Session-to-session coordination notes |
|
|
124
|
+
| `brain_sync` | Context Gateway: compact task memory, active-task peers, structured conflicts, automatic alerts, and timing |
|
|
89
125
|
| `brain_connect` | Find + draw related-but-unlinked cards |
|
|
90
126
|
| `canvas_view` | The board as an MCP App — Apps-capable chats get an interactive spatial view; everyone else gets clean text |
|
|
91
127
|
| `read_canvas` | A canvas as markdown (cards, connection graph, `[[links]]`, `#tags`) |
|
|
@@ -97,23 +133,25 @@ Then ask your agent things like *"what did we decide about auth?"*, *"challenge
|
|
|
97
133
|
|
|
98
134
|
### Tools vs. the *automatic* brain
|
|
99
135
|
|
|
100
|
-
This package is the **agent-neutral read/write surface** — any MCP client gets the tools above on demand (*pull*),
|
|
136
|
+
This package is the **agent-neutral read/write surface** — any MCP client gets the tools above on demand (*pull*), automatically registers presence for the lifetime of its authorized connection, and receives the Context Gateway workflow through standard MCP server instructions. `install` wires Claude Code's existing capture path and installs the local runtime; `link` projects repository-level MCP and instruction files for Codex and other coding agents. Codex and other clients retrieve relevant project memory and coordinate task intent/files through one `brain_sync` call, while `brain_note` captures durable decisions explicitly; optional host hooks add mechanically guaranteed lifecycle capture.
|
|
101
137
|
|
|
102
138
|
### Agent-neutral live presence
|
|
103
139
|
|
|
104
|
-
An active session means
|
|
105
|
-
recent-chat list is history, not presence.
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
An active session means an authorized MCP connection or host lifecycle adapter
|
|
141
|
+
heartbeated within 10 minutes; a row in a recent-chat list is history, not presence.
|
|
142
|
+
Each MCP connection registers at initialization and removes itself on disconnect.
|
|
143
|
+
Optional host adapters merge into that same logical session rather than double-counting
|
|
144
|
+
it, enrich it with intent/files, and remove only their own channel. The TTL covers crashes.
|
|
108
145
|
|
|
109
|
-
Future hosts
|
|
110
|
-
|
|
111
|
-
`
|
|
112
|
-
|
|
146
|
+
Future hosts get baseline support merely by connecting the MCP server. A deeper adapter
|
|
147
|
+
can import `klypix-mcp/presence` and map lifecycle events to `upsertSession`,
|
|
148
|
+
`removeSession`, `peekMessages`, and `receiveMessages`. The shared contract accepts `id`, `client`,
|
|
149
|
+
`surface`, `model`, `branch`, `intent`, touched `files`, and adapter `channel`;
|
|
150
|
+
host-specific transcript parsing stays outside the protocol.
|
|
113
151
|
|
|
114
152
|
### Updates — the propagation contract
|
|
115
153
|
|
|
116
|
-
`install` lays the whole brain (hooks + engine + local MCP server) into `~/.claude/project-brain
|
|
154
|
+
`install` lays the whole brain (hooks + engine + local MCP server) into `~/.claude/project-brain` and keeps the current brain project's Codex config machine-portable. Updates are automatic through the Claude session-start hook: it checks npm (≤ once/24h, fail-open, disable with `KLYPIX_AUTO_UPDATE=0`) and self-installs newer releases. One honest caveat: a running stdio server can't hot-swap — a new binary loads on the next full app launch (or `/mcp` reconnect); `brain_doctor`'s RUNNING line tells you when that's needed.
|
|
117
155
|
|
|
118
156
|
## Also speaks A2A (Agent-to-Agent)
|
|
119
157
|
|
|
@@ -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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// npx klypix-mcp install # install / update the brain on this machine
|
|
11
11
|
// npx klypix-mcp install --force # overwrite even a newer / dev-deployed brain
|
|
12
|
+
// npx klypix-mcp install --codex-hooks # optional prompt/file awareness; Codex asks for trust
|
|
12
13
|
//
|
|
13
14
|
// Never-throws-silently: it's an explicit CLI, so it reports what it did and exits
|
|
14
15
|
// non-zero on a real failure. Never wires a broken settings.json (refuse + restore).
|
|
@@ -18,10 +19,14 @@ import path from 'path';
|
|
|
18
19
|
import { fileURLToPath } from 'url';
|
|
19
20
|
import {
|
|
20
21
|
connectCodexMcpServer,
|
|
22
|
+
disconnectCodexMcpServer,
|
|
21
23
|
mcpServerEntry,
|
|
22
24
|
mergeCodexGlobalInstructions,
|
|
23
25
|
} from '../src/agent-rules.mjs';
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
codexPresenceHookStatus,
|
|
28
|
+
mergeCodexPresenceHooks,
|
|
29
|
+
} from '../src/codex-hooks.mjs';
|
|
25
30
|
|
|
26
31
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
32
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
@@ -29,6 +34,7 @@ const SRC = path.join(PKG_ROOT, 'src');
|
|
|
29
34
|
const BIN = path.join(PKG_ROOT, 'bin');
|
|
30
35
|
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version || ''; } catch { return ''; } })();
|
|
31
36
|
const FORCE = process.argv.includes('--force');
|
|
37
|
+
const CODEX_HOOKS = process.argv.includes('--codex-hooks');
|
|
32
38
|
|
|
33
39
|
const HOME = os.homedir();
|
|
34
40
|
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
@@ -38,44 +44,96 @@ const CODEX_CONFIG = path.join(HOME, '.codex', 'config.toml');
|
|
|
38
44
|
const HOOK_MARK = 'global-brain-hook';
|
|
39
45
|
const exists = (p) => { try { fs.statSync(p); return true; } catch { return false; } };
|
|
40
46
|
const fwd = (p) => p.replace(/\\/g, '/');
|
|
47
|
+
const copyRetrySleepSync = (ms) => {
|
|
48
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
49
|
+
catch { /* best effort */ }
|
|
50
|
+
};
|
|
51
|
+
function copyFileRobust(src, dest, tries = 8) {
|
|
52
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
53
|
+
try { fs.copyFileSync(src, dest); return; }
|
|
54
|
+
catch (error) {
|
|
55
|
+
const retryable = ['EBUSY', 'EPERM', 'EACCES'].includes(error?.code);
|
|
56
|
+
if (!retryable || attempt === tries) throw error;
|
|
57
|
+
copyRetrySleepSync(20 * attempt);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
41
61
|
function copyDir(src, dest) {
|
|
42
62
|
fs.mkdirSync(dest, { recursive: true });
|
|
43
63
|
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
44
64
|
const s = path.join(src, e.name), d = path.join(dest, e.name);
|
|
45
|
-
if (e.isDirectory()) copyDir(s, d); else if (e.isFile())
|
|
65
|
+
if (e.isDirectory()) copyDir(s, d); else if (e.isFile()) copyFileRobust(s, d);
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
// Codex has
|
|
50
|
-
//
|
|
51
|
-
//
|
|
69
|
+
// Codex has two approval-free layers and one optional native layer:
|
|
70
|
+
// project-scoped MCP gives it tools + live presence + brain_sync, while the
|
|
71
|
+
// conditional ~/.codex/AGENTS.md block makes the Context Gateway workflow
|
|
72
|
+
// automatic on every task. Native lifecycle hooks add mechanical prompt/file
|
|
73
|
+
// capture only when the user explicitly asks for --codex-hooks and approves them
|
|
74
|
+
// in a Codex surface that exposes trust review.
|
|
52
75
|
function wireCodex() {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
const projectDir = process.cwd();
|
|
77
|
+
const hasProjectBrain = exists(path.join(projectDir, 'brain.klypix'))
|
|
78
|
+
|| exists(path.join(projectDir, 'brain.any'));
|
|
79
|
+
const projectConfig = path.join(projectDir, '.codex', 'config.toml');
|
|
80
|
+
const mcp = hasProjectBrain
|
|
81
|
+
? connectCodexMcpServer({
|
|
82
|
+
configPath: projectConfig,
|
|
83
|
+
// Project config is commonly committed. Keep it portable: the
|
|
84
|
+
// package invocation that is running this installer is exactly the
|
|
85
|
+
// version the project asked for, while an absolute home path would
|
|
86
|
+
// dirty the repo and break it on every other machine.
|
|
87
|
+
entry: { command: 'npx', args: ['-y', 'klypix-mcp', '--vault', '.'] },
|
|
88
|
+
cwd: '..',
|
|
89
|
+
})
|
|
90
|
+
: { ok: true, action: 'skipped-no-project-brain', path: projectConfig };
|
|
91
|
+
// Pre-1.35 installed a global `--vault "."` entry. A global Codex process
|
|
92
|
+
// resolves that dot from the app install directory, not the user's project,
|
|
93
|
+
// so it can silently bind the wrong brain and override the correct project
|
|
94
|
+
// table. Remove only KLYPIX-owned global tables; preserve every other server.
|
|
95
|
+
const globalMcp = disconnectCodexMcpServer({ configPath: CODEX_CONFIG });
|
|
57
96
|
const instructions = mergeCodexGlobalInstructions(HOME);
|
|
58
97
|
const hookScript = path.join(BRAIN_DIR, 'codex-brain-hook.mjs');
|
|
59
|
-
const presence = exists(hookScript)
|
|
98
|
+
const presence = CODEX_HOOKS && exists(hookScript)
|
|
60
99
|
? mergeCodexPresenceHooks({
|
|
61
100
|
home: HOME,
|
|
62
101
|
command: `node "${fwd(hookScript)}"`,
|
|
63
102
|
})
|
|
64
|
-
: {
|
|
65
|
-
|
|
103
|
+
: {
|
|
104
|
+
ok: true,
|
|
105
|
+
action: codexPresenceHookStatus(HOME).installed ? 'existing' : 'off',
|
|
106
|
+
...codexPresenceHookStatus(HOME),
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
mcp,
|
|
110
|
+
globalMcp,
|
|
111
|
+
instructions,
|
|
112
|
+
presence,
|
|
113
|
+
ok: mcp.ok && globalMcp.ok && instructions.ok && presence.ok,
|
|
114
|
+
};
|
|
66
115
|
}
|
|
67
116
|
|
|
68
117
|
function reportCodex(result) {
|
|
69
118
|
if (result.ok) {
|
|
70
|
-
const
|
|
71
|
-
? '
|
|
72
|
-
: `
|
|
73
|
-
|
|
119
|
+
const mcp = result.mcp.action === 'skipped-no-project-brain'
|
|
120
|
+
? 'project MCP skipped (run `npx klypix-mcp link` inside a brain project)'
|
|
121
|
+
: `project MCP + automatic presence/Context Gateway (${result.mcp.action})`;
|
|
122
|
+
const enhanced = result.presence.action === 'off'
|
|
123
|
+
? 'enhanced hooks off (optional)'
|
|
124
|
+
: result.presence.action === 'existing'
|
|
125
|
+
? `enhanced hooks already configured (${result.presence.executionStatus || 'unverified'})`
|
|
126
|
+
: `enhanced hooks ${result.presence.action} — approve/review them in Codex /hooks`;
|
|
127
|
+
console.log(`✓ wired Codex: ${mcp} + approval-free task guidance (${result.instructions.action}) + ${enhanced}`);
|
|
128
|
+
if (result.globalMcp.action === 'disconnected') {
|
|
129
|
+
console.log('✓ removed obsolete global Codex KLYPIX MCP entry (it could resolve "." outside the project); other MCP servers were preserved');
|
|
130
|
+
}
|
|
74
131
|
return;
|
|
75
132
|
}
|
|
76
133
|
if (!result.mcp.ok) console.error(`⚠ Codex MCP was not changed: ${result.mcp.error}`);
|
|
134
|
+
if (!result.globalMcp.ok) console.error(`⚠ obsolete global Codex MCP entry was not removed: ${result.globalMcp.error}`);
|
|
77
135
|
if (!result.instructions.ok) console.error(`⚠ Codex guidance was not changed: ${result.instructions.error}`);
|
|
78
|
-
if (!result.presence.ok) console.error(`⚠ Codex
|
|
136
|
+
if (!result.presence.ok) console.error(`⚠ Codex enhanced hooks were not changed: ${result.presence.error}`);
|
|
79
137
|
console.error(' Claude Code installation is intact. Fix the Codex warning, then re-run this command.');
|
|
80
138
|
}
|
|
81
139
|
|
|
@@ -134,6 +192,10 @@ function migrateProjectMcpConfig() {
|
|
|
134
192
|
const entry = servers && servers['klypix-canvas'];
|
|
135
193
|
if (!entry || (entry.command !== 'npx' && entry.command !== 'node')) return null; // absent / hand-customized → leave it
|
|
136
194
|
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
195
|
+
// A repo-relative node launch (for example scripts/klypix-mcp-server.mjs)
|
|
196
|
+
// is deliberately portable and often tracks a project-owned bundle.
|
|
197
|
+
// Never replace it with a machine-specific ~/.claude path.
|
|
198
|
+
if (entry.command === 'node' && args[0] && !path.isAbsolute(String(args[0]))) return null;
|
|
137
199
|
const vi = args.indexOf('--vault');
|
|
138
200
|
const vault = vi >= 0 && args[vi + 1] ? args[vi + 1] : '.';
|
|
139
201
|
const next = mcpServerEntry({ vault }); // local now that the bundle is installed
|
|
@@ -154,7 +216,7 @@ const flatten = (code) => code
|
|
|
154
216
|
.replace(/\.\.\/src\/klypix-(core|format)\.mjs/g, './klypix-$1.mjs')
|
|
155
217
|
// brain-doctor + agent-rules (the server's lazy `import('../src/brain-doctor.mjs')`
|
|
156
218
|
// for the brain_doctor tool) → flat sibling refs in the runtime layout.
|
|
157
|
-
.replace(/\.\.\/src\/(brain-doctor|agent-rules)\.mjs/g, './$1.mjs')
|
|
219
|
+
.replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence)\.mjs/g, './$1.mjs')
|
|
158
220
|
.replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
|
|
159
221
|
|
|
160
222
|
const gotLock = acquireLock();
|
|
@@ -211,10 +273,14 @@ try {
|
|
|
211
273
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
212
274
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
213
275
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
214
|
-
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', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
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']) {
|
|
215
277
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
216
278
|
}
|
|
217
|
-
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
|
+
]) {
|
|
218
284
|
const s = path.join(BIN, src); if (exists(s)) staged.push({ dst, content: flatten(fs.readFileSync(s, 'utf8')) });
|
|
219
285
|
}
|
|
220
286
|
for (const st of staged) fs.writeFileSync(path.join(BRAIN_DIR, st.dst + '.klypix-new'), st.content);
|
|
@@ -282,9 +348,10 @@ try {
|
|
|
282
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}.`);
|
|
283
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.`);
|
|
284
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.`);
|
|
285
|
-
console.log(' Claude Code keeps its existing auto-brief/capture hooks; Codex
|
|
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.');
|
|
286
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.');
|
|
287
|
-
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`.');
|
|
288
355
|
} catch (e) {
|
|
289
356
|
if (gotLock) releaseLock();
|
|
290
357
|
console.error(`✗ install failed: ${e?.message || e}`);
|
package/bin/klypix-link.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Synchronous top-level by design: the dispatcher does `await import(this); process.exit(0)`,
|
|
18
18
|
// so all work (and its logs) must complete during module evaluation, before exit.
|
|
19
19
|
import path from 'path';
|
|
20
|
-
import { linkProject } from '../src/agent-rules.mjs';
|
|
20
|
+
import { compactAgentsBrief, linkProject } from '../src/agent-rules.mjs';
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
23
|
const args = process.argv.slice(3);
|
|
@@ -25,6 +25,7 @@ try {
|
|
|
25
25
|
const dirArg = args.find(a => !a.startsWith('-'));
|
|
26
26
|
const projectDir = path.resolve(dirArg || process.cwd());
|
|
27
27
|
const { rules, mcp, hasBrain, version } = linkProject(projectDir, { check });
|
|
28
|
+
const compactBrief = await compactAgentsBrief(projectDir, { check });
|
|
28
29
|
|
|
29
30
|
if (check) {
|
|
30
31
|
// Audit-only: classify, report, exit 1 on any drift.
|
|
@@ -34,7 +35,11 @@ try {
|
|
|
34
35
|
for (const r of mcp) console.log(` ${mark(r.status)} ${r.tool.padEnd(26)} ${r.file} — ${r.status.toUpperCase()}${r.why ? ' (' + r.why + ')' : ''}`);
|
|
35
36
|
console.log('\n Rules / instructions:');
|
|
36
37
|
for (const r of rules) console.log(` ${mark(r.status)} ${r.tool.padEnd(26)} ${r.file} — ${r.status.toUpperCase()}${r.stampedVersion ? ' (stamped v' + r.stampedVersion + ')' : ''}`);
|
|
37
|
-
|
|
38
|
+
if (compactBrief.status !== 'skipped') {
|
|
39
|
+
console.log(` ${mark(compactBrief.status)} ${'AGENTS.md compact fallback'.padEnd(26)} AGENTS.md — ${compactBrief.status.toUpperCase()}${compactBrief.bytes ? ` (${compactBrief.bytes} bytes)` : ''}`);
|
|
40
|
+
}
|
|
41
|
+
const briefDrift = ['stale', 'error'].includes(compactBrief.status) ? [compactBrief] : [];
|
|
42
|
+
const drift = [...rules, ...mcp, ...briefDrift].filter(r => r.status !== 'ok' && r.status !== 'skipped');
|
|
38
43
|
if (!drift.length) { console.log(`\n✓ in sync — all ${rules.length + mcp.length} managed file(s) match brain v${version}.`); process.exit(0); }
|
|
39
44
|
console.log(`\n✗ ${drift.length} file(s) drifted (stale / hand-edited / missing) — run \`npx klypix-mcp link\` to re-project.`);
|
|
40
45
|
process.exit(1);
|
|
@@ -46,8 +51,11 @@ try {
|
|
|
46
51
|
for (const r of mcp) console.log(` ${mark(r.action)} ${r.tool.padEnd(26)} ${r.file}${r.why ? ' (' + r.why + ')' : ''}`);
|
|
47
52
|
console.log('\n Rules (so each tool auto-reads + captures the brain):');
|
|
48
53
|
for (const r of rules) console.log(` ${mark(r.action)} ${r.tool.padEnd(26)} ${r.file}${r.why ? ' (' + r.why + ')' : ''}`);
|
|
54
|
+
if (compactBrief.status !== 'skipped') {
|
|
55
|
+
console.log(` ${mark(compactBrief.action)} ${'AGENTS.md compact fallback'.padEnd(26)} AGENTS.md${compactBrief.bytes ? ` (${compactBrief.bytes} bytes)` : ''}`);
|
|
56
|
+
}
|
|
49
57
|
|
|
50
|
-
const changed = [...rules, ...mcp].filter(r => r.action && !['unchanged', 'skipped'].includes(r.action)).length;
|
|
58
|
+
const changed = [...rules, ...mcp, compactBrief].filter(r => r.action && !['unchanged', 'skipped'].includes(r.action)).length;
|
|
51
59
|
console.log(`\n✓ ${changed} file(s) written/updated — every agent opened in this project now reads + captures ./brain.klypix.`);
|
|
52
60
|
console.log(' Codex gets .codex/config.toml + AGENTS.md; Cline & Windsurf use their global MCP config plus project rules.');
|
|
53
61
|
console.log(' Verify anytime with `npx klypix-mcp link --check` (or `npx klypix-mcp doctor`).');
|