klypix-mcp 1.70.0 → 1.72.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 +48 -20
- package/bin/klypix-install.mjs +69 -6
- package/bin/klypix-mcp.mjs +2 -1
- package/bin/klypix-worker.mjs +2 -1
- package/package.json +2 -2
- package/src/agent-rules.mjs +67 -17
- package/src/editor-detect.mjs +117 -0
- package/src/mcp-presence.mjs +291 -18
- package/src/mcp-verify.mjs +136 -0
- package/src/repo-state.mjs +273 -4
- package/src/setup.mjs +286 -0
package/README.md
CHANGED
|
@@ -85,28 +85,55 @@ It verifies 15 required coordination behaviours — not the 26 tools, and not th
|
|
|
85
85
|
|
|
86
86
|
## Quick start
|
|
87
87
|
|
|
88
|
-
**
|
|
88
|
+
Run this **inside your project**:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
npx klypix-mcp install
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
One command, every editor. It finds the project root (walking up, so running it from `src/` is
|
|
95
|
+
fine), gives the project a brain if it doesn't have one, wires the agent tools you actually have
|
|
96
|
+
installed, registers the lossless `.klypix` merge driver if it's a git repo, and then **proves the
|
|
97
|
+
result** before it exits:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
project E:\work\api (git repository root)
|
|
101
|
+
brain created brain.klypix — a starter brain, ready for its first decision
|
|
102
|
+
editors Claude Code · Cursor · Codex · Gemini CLI · Antigravity · VS Code
|
|
103
|
+
wired 9 file(s) · 9 updated (skipped 5 for tools you don't have)
|
|
104
|
+
git lossless .klypix merge driver registered
|
|
105
|
+
verified ✓ 26 tools reachable via .mcp.json (892ms)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That last line is the point. MCP config fails **silently** — a wrong entry means the server never
|
|
109
|
+
starts, the agent quietly loses every brain verb, and nothing reports an error. So `install` opens
|
|
110
|
+
a real stdio handshake against the config it just wrote and counts the tools that answered. A
|
|
111
|
+
broken entry dies in ~100ms with `Connection closed` and is reported, not shipped.
|
|
112
|
+
|
|
113
|
+
What goes where:
|
|
114
|
+
|
|
115
|
+
- **Machine-global, once** — the engine + runtime in `~/.claude/project-brain`, Claude Code's four
|
|
116
|
+
lifecycle hooks in `~/.claude/settings.json`, and the `~/.codex/AGENTS.md` guidance block. Claude
|
|
117
|
+
Code is therefore covered in every project on that machine that has a `./brain.klypix`.
|
|
118
|
+
- **Per project** — MCP config and rules for Cursor, Codex, Cline, Windsurf, Copilot, Gemini CLI /
|
|
119
|
+
Antigravity and Aider. Run `install` once inside each project.
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
Three things it deliberately will **not** do:
|
|
99
122
|
|
|
100
|
-
- **
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
- **Write for editors you don't have.** Config is projected only for hosts detected on this
|
|
124
|
+
machine — a two-person team using one editor no longer commits rules for six they never opened.
|
|
125
|
+
A file your project *already* carries stays maintained regardless, so you can't silently stop
|
|
126
|
+
updating your team's committed configs.
|
|
127
|
+
- **Wire a directory that isn't a project.** It refuses your home folder, a drive root, and
|
|
128
|
+
anything with no brain, no git repo and no project manifest. A mistyped command can't seed a
|
|
129
|
+
brain into `C:\Users\you`.
|
|
130
|
+
- **Replace a project-owned server.** A repo-relative launch like
|
|
131
|
+
`node scripts/klypix-mcp-server.mjs` is deliberate — it resolves offline and rides a bundle the
|
|
132
|
+
repo version-gates — so it's left byte-identical and reported. An explicit `link` still rewrites
|
|
133
|
+
everything: an action you didn't ask for stays more conservative than one you did.
|
|
108
134
|
|
|
109
|
-
|
|
135
|
+
Opt out with `--no-project` (CI images, scripted provisioning). `--json` emits the report as
|
|
136
|
+
structured data; `--verify-all` handshakes every written config instead of one.
|
|
110
137
|
|
|
111
138
|
Optional, opt-in, and approved inside Codex itself:
|
|
112
139
|
|
|
@@ -119,15 +146,16 @@ file-overlap warning. Codex owns the trust decision and will ask you to review t
|
|
|
119
146
|
`brain_doctor` reports this layer separately as off, execution-unverified, or active. Even with it
|
|
120
147
|
on, **Codex never captures decisions automatically** — the Codex hook never writes the brain.
|
|
121
148
|
|
|
122
|
-
**
|
|
149
|
+
**Re-project everything explicitly:**
|
|
123
150
|
|
|
124
151
|
```bash
|
|
125
152
|
npx klypix-mcp link
|
|
126
153
|
```
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
`install` already does this for the editors you have. Reach for `link` when you want all 14
|
|
156
|
+
managed, hash-stamped files regardless of what's installed — MCP server config for six hosts plus
|
|
157
|
+
rules files for eight — or to repair drift. Managed blocks are merged into your existing
|
|
158
|
+
instruction files and never clobber your content.
|
|
131
159
|
|
|
132
160
|
```bash
|
|
133
161
|
npx klypix-mcp link --check # audits without writing; exits non-zero on drift
|
|
@@ -481,8 +509,8 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
481
509
|
| Command | What it does |
|
|
482
510
|
|---|---|
|
|
483
511
|
| `npx klypix-mcp init` | Seed a starter `brain.klypix` here and print an MCP config |
|
|
484
|
-
| `npx klypix-mcp install` |
|
|
485
|
-
| `npx klypix-mcp link` |
|
|
512
|
+
| `npx klypix-mcp install` | Set up everything: machine engine + hooks, then this project — brain, config for the editors you have, merge driver, verified (see Quick start) |
|
|
513
|
+
| `npx klypix-mcp link` | Re-project all 14 managed files regardless of what is installed (`--check` audits) |
|
|
486
514
|
| `npx klypix-mcp doctor` | One verdict: version, hosts, live sessions, tool count, drift. Exits non-zero — usable as a CI gate |
|
|
487
515
|
| `npx klypix-mcp runtime` | Passive per-connection process/RAM attribution (`--json`, optional `--watch seconds`); never kills or deduplicates |
|
|
488
516
|
| `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { brainInstallDecision, deploySourceDecision } from '../src/install-version.mjs';
|
|
35
35
|
import { acquireInstallLockSync, releaseInstallLockSync } from '../src/install-lock.mjs';
|
|
36
36
|
import { collectRepoState } from '../src/repo-state.mjs';
|
|
37
|
+
import { runSetup, renderBrief } from '../src/setup.mjs';
|
|
37
38
|
|
|
38
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
40
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
@@ -48,6 +49,12 @@ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.join(PKG_R
|
|
|
48
49
|
const FORCE = process.argv.includes('--force');
|
|
49
50
|
const CODEX_HOOKS = process.argv.includes('--codex-hooks');
|
|
50
51
|
const RUNTIME_ONLY = process.argv.includes('--runtime-only');
|
|
52
|
+
// Project wiring is the default because it is the step users did not know
|
|
53
|
+
// existed. These opt OUT for the cases that genuinely want machine-only:
|
|
54
|
+
// CI images, scripted provisioning, and anyone wiring the project by hand.
|
|
55
|
+
const NO_PROJECT = process.argv.includes('--no-project');
|
|
56
|
+
const VERIFY_ALL = process.argv.includes('--verify-all');
|
|
57
|
+
const SETUP_JSON = process.argv.includes('--json');
|
|
51
58
|
// Released-tag deploy-guard acknowledgement. Deliberately a SEPARATE axis from
|
|
52
59
|
// --force: --force is destination authority (overwrite what is installed),
|
|
53
60
|
// this is source authority (knowingly deploy an untagged working tree).
|
|
@@ -64,7 +71,7 @@ const CODEX_CONFIG = path.join(HOME, '.codex', 'config.toml');
|
|
|
64
71
|
const HOOK_MARK = 'global-brain-hook';
|
|
65
72
|
const exists = (p) => { try { fs.statSync(p); return true; } catch { return false; } };
|
|
66
73
|
const fwd = (p) => p.replace(/\\/g, '/');
|
|
67
|
-
const
|
|
74
|
+
const retrySleepSync = (ms) => {
|
|
68
75
|
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
69
76
|
catch { /* best effort */ }
|
|
70
77
|
};
|
|
@@ -74,7 +81,7 @@ function copyFileRobust(src, dest, tries = 8) {
|
|
|
74
81
|
catch (error) {
|
|
75
82
|
const retryable = ['EBUSY', 'EPERM', 'EACCES'].includes(error?.code);
|
|
76
83
|
if (!retryable || attempt === tries) throw error;
|
|
77
|
-
|
|
84
|
+
retrySleepSync(20 * attempt);
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
}
|
|
@@ -167,6 +174,44 @@ function reportCodex(result) {
|
|
|
167
174
|
// update hook runs install per project, so every project heals once). Only the
|
|
168
175
|
// known npx/node launch is migrated; a hand-customized command / invalid JSON is
|
|
169
176
|
// left untouched, and the original is backed up. Best-effort, never throws.
|
|
177
|
+
// ── Windows rename hardening (1.71.1) ───────────────────────────────────────
|
|
178
|
+
// Every atomic commit in this installer is a rename-over-destination, and on
|
|
179
|
+
// Windows that throws EPERM/EBUSY/EACCES while ANY process briefly holds the
|
|
180
|
+
// target — an AV scan, an indexer, or, routinely on a developer machine, the
|
|
181
|
+
// live MCP servers reading the very bundle we are replacing. Measured 2026-08-15:
|
|
182
|
+
// three failures in one session with 7-14 servers live, on .mcp-runtime.json and
|
|
183
|
+
// brain-history.mjs; each retry succeeded immediately.
|
|
184
|
+
//
|
|
185
|
+
// The failure is loud rather than silent, and a manual re-run fixes it — but
|
|
186
|
+
// this is the FIRST command a new user types, and a raw EPERM stack at that
|
|
187
|
+
// moment is a bad first contact. So the installer now outlasts a transient
|
|
188
|
+
// holder using the SAME bounded backoff the brain write funnel has used since
|
|
189
|
+
// 1.68.0 (klypix-format.mjs atomicWrite): ~2.7s total, then rethrow. A
|
|
190
|
+
// persistent holder still fails loudly — delayed is acceptable, silently wrong
|
|
191
|
+
// is not, and pretending we wrote a file we did not would be far worse.
|
|
192
|
+
const RENAME_RETRYABLE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
193
|
+
const RENAME_BACKOFF_MS = [40, 120, 300, 700, 1500];
|
|
194
|
+
/** fs.renameSync that outlasts a transient Windows lock on the destination. */
|
|
195
|
+
function renameSyncWithBackoff(from, to) {
|
|
196
|
+
for (let attempt = 0; ; attempt++) {
|
|
197
|
+
try { return fs.renameSync(from, to); }
|
|
198
|
+
catch (e) {
|
|
199
|
+
if (attempt >= RENAME_BACKOFF_MS.length || !RENAME_RETRYABLE_CODES.has(e?.code)) {
|
|
200
|
+
// Name the real cause: "EPERM: operation not permitted" tells a
|
|
201
|
+
// user nothing actionable, and the fix is usually to close the
|
|
202
|
+
// editors whose servers hold the bundle.
|
|
203
|
+
if (RENAME_RETRYABLE_CODES.has(e?.code)) {
|
|
204
|
+
e.message = `${e.message}\n ↳ ${path.basename(to)} is held by another process after ~2.7s of retries.`
|
|
205
|
+
+ '\n On Windows this is normally a running MCP server, an antivirus scan, or an indexer.'
|
|
206
|
+
+ '\n Close your editors (or quit KLYPIX) and run the command again — nothing was left half-written.';
|
|
207
|
+
}
|
|
208
|
+
throw e;
|
|
209
|
+
}
|
|
210
|
+
retrySleepSync(RENAME_BACKOFF_MS[attempt]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
170
215
|
function migrateProjectMcpConfig() {
|
|
171
216
|
try {
|
|
172
217
|
const file = path.join(process.cwd(), '.mcp.json');
|
|
@@ -355,7 +400,7 @@ try {
|
|
|
355
400
|
} catch { /* .prev rollback snapshot is best-effort */ }
|
|
356
401
|
const renameOrder = staged.slice().sort((a, b) => (a.dst === 'global-brain-hook.mjs' ? 1 : 0) - (b.dst === 'global-brain-hook.mjs' ? 1 : 0));
|
|
357
402
|
let n = 0;
|
|
358
|
-
for (const st of renameOrder) {
|
|
403
|
+
for (const st of renameOrder) { renameSyncWithBackoff(path.join(BRAIN_DIR, st.dst + '.klypix-new'), path.join(BRAIN_DIR, st.dst)); n++; }
|
|
359
404
|
|
|
360
405
|
// 3) mark the dir an ESM package
|
|
361
406
|
fs.writeFileSync(path.join(BRAIN_DIR, 'package.json'), JSON.stringify({ name: 'klypix-project-brain', private: true, type: 'module' }, null, 2));
|
|
@@ -390,7 +435,7 @@ try {
|
|
|
390
435
|
const tmp = SETTINGS + '.klypix-tmp';
|
|
391
436
|
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
|
|
392
437
|
JSON.parse(fs.readFileSync(tmp, 'utf8')); // verify before swap
|
|
393
|
-
|
|
438
|
+
renameSyncWithBackoff(tmp, SETTINGS);
|
|
394
439
|
}
|
|
395
440
|
|
|
396
441
|
// 6) Commit the runtime pointer and version receipt atomically while the
|
|
@@ -417,7 +462,7 @@ try {
|
|
|
417
462
|
};
|
|
418
463
|
const runtimePath = path.join(BRAIN_DIR, '.mcp-runtime.json');
|
|
419
464
|
fs.writeFileSync(runtimePath + '.klypix-new', JSON.stringify(runtime, null, 2) + '\n', 'utf8');
|
|
420
|
-
|
|
465
|
+
renameSyncWithBackoff(runtimePath + '.klypix-new', runtimePath);
|
|
421
466
|
// A tagged-but-DIRTY checkout keeps via:'npm' (the tag still names the
|
|
422
467
|
// payload identity, and dev:true would stop auto-update from healing the
|
|
423
468
|
// machine back to clean released bytes) but stamps dirty:true + the audit
|
|
@@ -430,7 +475,7 @@ try {
|
|
|
430
475
|
: { brainVersion: VERSION, via: 'npm', dirty: false, installedAt };
|
|
431
476
|
const versionPath = path.join(BRAIN_DIR, '.brain-version.json');
|
|
432
477
|
fs.writeFileSync(versionPath + '.klypix-new', JSON.stringify(versionStamp, null, 2), 'utf8');
|
|
433
|
-
|
|
478
|
+
renameSyncWithBackoff(versionPath + '.klypix-new', versionPath);
|
|
434
479
|
|
|
435
480
|
// 7) migrate THIS project's .mcp.json off npx onto the now-installed local bundle
|
|
436
481
|
// (heals an existing stale config so the next MCP server spawn runs current).
|
|
@@ -480,6 +525,24 @@ try {
|
|
|
480
525
|
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.');
|
|
481
526
|
}
|
|
482
527
|
console.log(' Compatible brain-core updates hot-swap behind the same MCP connection. Only the one-time legacy→supervisor migration, a supervisor change, or an intentionally breaking tool/protocol change needs reconnect.');
|
|
528
|
+
|
|
529
|
+
// 9) PROJECT setup (1.71.0) — the step users never knew they had to take.
|
|
530
|
+
// `install` wired the machine; without this it wired nothing a Cursor,
|
|
531
|
+
// Antigravity, Codex, Cline or Copilot user could see, and the failure
|
|
532
|
+
// was silent. A runtime-only refresh deliberately skips it: that path
|
|
533
|
+
// exists to preserve every host/project config byte.
|
|
534
|
+
if (!RUNTIME_ONLY && !NO_PROJECT) {
|
|
535
|
+
try {
|
|
536
|
+
const report = await runSetup({ verifyAll: VERIFY_ALL });
|
|
537
|
+
if (SETUP_JSON) console.log(JSON.stringify(report, null, 2));
|
|
538
|
+
else console.log(renderBrief(report));
|
|
539
|
+
} catch (e) {
|
|
540
|
+
// The machine install already succeeded and is independently
|
|
541
|
+
// useful; a project-wiring failure must report itself, not undo it.
|
|
542
|
+
console.error(`⚠ project setup could not finish: ${e?.message || e}`);
|
|
543
|
+
console.error(' The machine install is intact — re-run inside your project, or use `npx klypix-mcp link`.');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
483
546
|
console.log(' Verify anytime: `npx klypix-mcp doctor`; prove two-client behavior with `npx klypix-mcp conformance`.');
|
|
484
547
|
} catch (e) {
|
|
485
548
|
releaseInstallLockSync(installLock);
|
package/bin/klypix-mcp.mjs
CHANGED
|
@@ -25,7 +25,8 @@ const USAGE = [
|
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
26
26
|
'',
|
|
27
27
|
'Verbs:',
|
|
28
|
-
' install [--force] [--codex-hooks]
|
|
28
|
+
' install [--force] [--codex-hooks] set up everything: machine engine + Claude hooks, then THIS project —',
|
|
29
|
+
' [--no-project] [--json] brain, config for the editors you actually have, merge driver, verified',
|
|
29
30
|
' link [dir] [--check] project this project\'s 14 managed agent config files (--check audits, writes nothing, exits 1 on drift)',
|
|
30
31
|
' doctor [--npm] [--all] [--json] read-only self-check; exits 1 on drift',
|
|
31
32
|
' runtime [--json] [--watch seconds] passive MCP process/RAM attribution; never terminates a process',
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -876,7 +876,8 @@ server.registerTool('brain_sync', {
|
|
|
876
876
|
releaseIntent: z.object({
|
|
877
877
|
version: z.string().max(64).describe('The version this session intends to release (e.g. "1.70.0").'),
|
|
878
878
|
ref: z.string().max(200).describe('The git ref (branch or tag) the release will be cut from.'),
|
|
879
|
-
|
|
879
|
+
acknowledge: z.array(z.string().max(40)).max(64).optional().describe('Commit shas this release DELIBERATELY leaves behind. Only needed after a refusal: if the ref would drop finished work, the lease is refused and the response names every sha. Re-declare with those shas here to proceed — and tell the user what they are first.'),
|
|
880
|
+
}).optional().describe('Declare EXCLUSIVE intent to prepare a release of this project. The first declarer takes a ~2h lease (refreshed by checkpoints, freed by phase "complete", by expiry, or when the holder session ends); a second declarer gets a structured hard conflict naming the holder, version, and ref. While any lease is active every peer\'s sync gains a "release in preparation" footer line. A NEW declaration is also checked against what the release would LEAVE BEHIND: if the ref is missing commits that are on trunk or on a branch a live peer session is working on, the lease is REFUSED (nothing is changed) and the response lists them — report those commits to the user, then re-declare with acknowledge:[...] naming each sha if the release should go ahead without them.'),
|
|
880
881
|
},
|
|
881
882
|
}, async ({ project, intent, files, phase, include_context, results, releaseIntent }, extra) => {
|
|
882
883
|
const totalStartedAt = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.72.0",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
85
85
|
"test:bench": "node test/bench.mjs",
|
|
86
86
|
"pretest": "node test/publish-workflow.mjs",
|
|
87
|
-
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.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/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.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/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
87
|
+
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.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/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.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/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-ancestry.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
88
88
|
"test:memory": "node test/memory-runtime.mjs",
|
|
89
89
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
90
90
|
"runtime": "node bin/klypix-runtime.mjs"
|
package/src/agent-rules.mjs
CHANGED
|
@@ -660,46 +660,73 @@ function classifyMcp(file, wrapKey, projectDir) {
|
|
|
660
660
|
|
|
661
661
|
// The projection map — the single source of truth shared by WRITE (linkProject) and
|
|
662
662
|
// CHECK (auditProject), so the two can never disagree about what's projected where.
|
|
663
|
+
//
|
|
664
|
+
// `editors` (1.71.0) names which detected hosts justify writing each file, so a
|
|
665
|
+
// project stops collecting managed config for tools nobody on the machine has.
|
|
666
|
+
// `standard: true` marks the cross-tool AGENTS.md convention — justified by ANY
|
|
667
|
+
// agent host rather than one specific id. Filtering only happens when a caller
|
|
668
|
+
// passes an explicit editor set; the default projects everything, exactly as
|
|
669
|
+
// before, so every existing caller is unaffected.
|
|
663
670
|
function targets(projectDir) {
|
|
664
671
|
const j = (...p) => path.join(projectDir, ...p);
|
|
665
672
|
return {
|
|
666
673
|
rules: [
|
|
667
|
-
{ tool: 'Codex / AGENTS.md standard', file: j('AGENTS.md'), kind: 'merge' },
|
|
668
|
-
{ tool: 'Cursor', file: j('.cursor', 'rules', 'klypix-brain.mdc'), kind: 'dedicated', frontmatter: '---\ndescription: KLYPIX project brain — read at task start, capture decisions\nalwaysApply: true\n---' },
|
|
669
|
-
{ tool: 'Windsurf', file: j('.windsurf', 'rules', 'klypix-brain.md'), kind: 'dedicated', frontmatter: '---\ntrigger: always_on\n---' },
|
|
670
|
-
{ tool: 'Cline', file: j('.clinerules', 'klypix-brain.md'), kind: 'dedicated', frontmatter: '' },
|
|
671
|
-
{ tool: 'GitHub Copilot', file: j('.github', 'copilot-instructions.md'), kind: 'merge' },
|
|
674
|
+
{ tool: 'Codex / AGENTS.md standard', file: j('AGENTS.md'), kind: 'merge', editors: ['codex'], standard: true },
|
|
675
|
+
{ tool: 'Cursor', file: j('.cursor', 'rules', 'klypix-brain.mdc'), kind: 'dedicated', editors: ['cursor'], frontmatter: '---\ndescription: KLYPIX project brain — read at task start, capture decisions\nalwaysApply: true\n---' },
|
|
676
|
+
{ tool: 'Windsurf', file: j('.windsurf', 'rules', 'klypix-brain.md'), kind: 'dedicated', editors: ['windsurf'], frontmatter: '---\ntrigger: always_on\n---' },
|
|
677
|
+
{ tool: 'Cline', file: j('.clinerules', 'klypix-brain.md'), kind: 'dedicated', editors: ['cline'], frontmatter: '' },
|
|
678
|
+
{ tool: 'GitHub Copilot', file: j('.github', 'copilot-instructions.md'), kind: 'merge', editors: ['copilot'] },
|
|
672
679
|
// Added 1.13.0 — close the "not generated at all" coverage gap the audit flagged.
|
|
673
|
-
{ tool: 'Gemini CLI', file: j('GEMINI.md'), kind: 'merge' },
|
|
674
|
-
{ tool: 'Aider', file: j('CONVENTIONS.md'), kind: 'dedicated', frontmatter: '' },
|
|
680
|
+
{ tool: 'Gemini CLI', file: j('GEMINI.md'), kind: 'merge', editors: ['gemini-cli', 'antigravity'] },
|
|
681
|
+
{ tool: 'Aider', file: j('CONVENTIONS.md'), kind: 'dedicated', editors: ['aider'], frontmatter: '' },
|
|
675
682
|
// Added 1.29.1 — Antigravity (Gemini IDE) reads .agents/AGENTS.md; the desktop
|
|
676
683
|
// app already writes it (projectBrainConnect.ts), this adds CLI audit coverage.
|
|
677
684
|
// kind:'merge' so the fenced block coexists with any existing content.
|
|
678
|
-
{ tool: 'Antigravity', file: j('.agents', 'AGENTS.md'), kind: 'merge' },
|
|
685
|
+
{ tool: 'Antigravity', file: j('.agents', 'AGENTS.md'), kind: 'merge', editors: ['antigravity'] },
|
|
679
686
|
// Intentionally NO project CLAUDE.md target. Claude receives the same task
|
|
680
687
|
// contract mechanically through lifecycle hooks + MCP tool instructions;
|
|
681
688
|
// the desktop legacy connector still owns CLAUDE.md, so a second Core
|
|
682
689
|
// writer here would create duelling managed blocks and update churn.
|
|
683
690
|
],
|
|
684
691
|
mcp: [
|
|
685
|
-
{ tool: 'Codex', file: j('.codex', 'config.toml'), format: 'toml' },
|
|
686
|
-
{ tool: 'Claude Code', file: j('.mcp.json'), wrapKey: 'mcpServers', withType: false },
|
|
687
|
-
{ tool: 'Cursor', file: j('.cursor', 'mcp.json'), wrapKey: 'mcpServers', withType: false },
|
|
688
|
-
{ tool: 'Cline', file: j('.cline', 'mcp.json'), wrapKey: 'mcpServers', withType: false },
|
|
689
|
-
{ tool: 'Gemini CLI / Antigravity', file: j('.gemini', 'settings.json'), wrapKey: 'mcpServers', withType: false },
|
|
690
|
-
{ tool: 'VS Code (Copilot/Continue)', file: j('.vscode', 'mcp.json'), wrapKey: 'servers', withType: true },
|
|
692
|
+
{ tool: 'Codex', file: j('.codex', 'config.toml'), format: 'toml', editors: ['codex'] },
|
|
693
|
+
{ tool: 'Claude Code', file: j('.mcp.json'), wrapKey: 'mcpServers', withType: false, editors: ['claude-code'] },
|
|
694
|
+
{ tool: 'Cursor', file: j('.cursor', 'mcp.json'), wrapKey: 'mcpServers', withType: false, editors: ['cursor'] },
|
|
695
|
+
{ tool: 'Cline', file: j('.cline', 'mcp.json'), wrapKey: 'mcpServers', withType: false, editors: ['cline'] },
|
|
696
|
+
{ tool: 'Gemini CLI / Antigravity', file: j('.gemini', 'settings.json'), wrapKey: 'mcpServers', withType: false, editors: ['gemini-cli', 'antigravity'] },
|
|
697
|
+
{ tool: 'VS Code (Copilot/Continue)', file: j('.vscode', 'mcp.json'), wrapKey: 'servers', withType: true, editors: ['vscode', 'copilot'] },
|
|
691
698
|
],
|
|
692
699
|
};
|
|
693
700
|
}
|
|
694
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Should this target be projected, given the hosts detected on the machine?
|
|
704
|
+
*
|
|
705
|
+
* Three independent yeses, in order of strength:
|
|
706
|
+
* 1. a detected host reads it;
|
|
707
|
+
* 2. it is the cross-tool AGENTS.md standard and SOME agent host is present;
|
|
708
|
+
* 3. the file already exists in the project — a teammate committed it, so it
|
|
709
|
+
* stays current whether or not THIS machine has that editor. Without this
|
|
710
|
+
* rule a one-editor developer would silently stop maintaining the configs
|
|
711
|
+
* the rest of their team depends on.
|
|
712
|
+
*/
|
|
713
|
+
function targetJustified(target, editors, anyEditor) {
|
|
714
|
+
if (!editors) return true; // unfiltered: project everything
|
|
715
|
+
if ((target.editors || []).some((id) => editors.has(id))) return true;
|
|
716
|
+
if (target.standard && anyEditor) return true;
|
|
717
|
+
return exists(target.file);
|
|
718
|
+
}
|
|
719
|
+
|
|
695
720
|
const relFile = (projectDir, abs) => path.relative(projectDir, abs).replace(/\\/g, '/');
|
|
696
721
|
|
|
697
722
|
/**
|
|
698
723
|
* Wire a project so EVERY agent tool reads + captures its brain automatically — or,
|
|
699
724
|
* with { check:true }, AUDIT the projection without touching disk.
|
|
700
725
|
* @param {string} projectDir absolute project root (holds ./brain.klypix)
|
|
701
|
-
* @param {{ version?: string, check?: boolean }} [opts]
|
|
702
|
-
*
|
|
726
|
+
* @param {{ version?: string, check?: boolean, editors?: Set<string>|string[] }} [opts]
|
|
727
|
+
* editors — when supplied, project only what a detected host actually reads
|
|
728
|
+
* (plus files the project already carries). Omit for the legacy write-all.
|
|
729
|
+
* @returns {{ rules, mcp, hasBrain, version, check, skipped: Array }}
|
|
703
730
|
*/
|
|
704
731
|
export function linkProject(projectDir, opts = {}) {
|
|
705
732
|
const version = opts.version || resolveVersion();
|
|
@@ -707,6 +734,29 @@ export function linkProject(projectDir, opts = {}) {
|
|
|
707
734
|
const t = targets(projectDir);
|
|
708
735
|
const hasBrain = exists(path.join(projectDir, 'brain.klypix')) || exists(path.join(projectDir, 'brain.any'));
|
|
709
736
|
|
|
737
|
+
const editors = opts.editors == null ? null
|
|
738
|
+
: (opts.editors instanceof Set ? opts.editors : new Set(opts.editors));
|
|
739
|
+
const anyEditor = !!editors && editors.size > 0;
|
|
740
|
+
// Files the caller has decided not to touch at all — used by the automatic
|
|
741
|
+
// setup path to leave a project-owned (vendored, repo-relative) server alone.
|
|
742
|
+
// An explicitly typed `link` still rewrites everything; an action the user
|
|
743
|
+
// did not ask for should be more conservative than one they did.
|
|
744
|
+
const exclude = opts.exclude instanceof Set ? opts.exclude
|
|
745
|
+
: new Set(Array.isArray(opts.exclude) ? opts.exclude : []);
|
|
746
|
+
const skipped = [];
|
|
747
|
+
const keep = (target) => {
|
|
748
|
+
const file = relFile(projectDir, target.file);
|
|
749
|
+
if (exclude.has(file)) {
|
|
750
|
+
skipped.push({ tool: target.tool, file, why: 'project-owned server — left untouched' });
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
if (targetJustified(target, editors, anyEditor)) return true;
|
|
754
|
+
skipped.push({ tool: target.tool, file, why: 'not installed on this machine' });
|
|
755
|
+
return false;
|
|
756
|
+
};
|
|
757
|
+
t.rules = t.rules.filter(keep);
|
|
758
|
+
t.mcp = t.mcp.filter(keep);
|
|
759
|
+
|
|
710
760
|
const rules = t.rules.map((r) => {
|
|
711
761
|
const file = relFile(projectDir, r.file);
|
|
712
762
|
try {
|
|
@@ -761,7 +811,7 @@ export function linkProject(projectDir, opts = {}) {
|
|
|
761
811
|
}
|
|
762
812
|
});
|
|
763
813
|
|
|
764
|
-
return { rules, mcp, hasBrain, version, check };
|
|
814
|
+
return { rules, mcp, hasBrain, version, check, skipped };
|
|
765
815
|
}
|
|
766
816
|
|
|
767
817
|
/**
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Which agent tools does this machine ACTUALLY have?
|
|
2
|
+
//
|
|
3
|
+
// Before 1.71 `link` projected all 14 managed files into every project, for
|
|
4
|
+
// every supported host, whether or not the user had ever installed it. Those
|
|
5
|
+
// files get COMMITTED, so a two-person team using one editor shipped rules and
|
|
6
|
+
// MCP config for six they had never opened — repo clutter that reads as noise
|
|
7
|
+
// in review and makes the tool look presumptuous on first contact.
|
|
8
|
+
//
|
|
9
|
+
// This module answers the narrower question the projection should have been
|
|
10
|
+
// asking: is this host present on this machine? Detection is deliberately
|
|
11
|
+
// EVIDENCE-BASED and conservative — every positive names the path or variable
|
|
12
|
+
// that produced it (`why`), so a wrong answer is auditable rather than
|
|
13
|
+
// mysterious. Absence is never proof; the caller pairs this with the
|
|
14
|
+
// project-level signal (a target file already committed) and keeps projecting
|
|
15
|
+
// anything a teammate already relies on.
|
|
16
|
+
//
|
|
17
|
+
// Pure except for fs existence checks, and every environment input is
|
|
18
|
+
// injectable so the whole matrix is testable without touching a real home dir.
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
|
|
23
|
+
/** Roaming-config root per platform — where Electron editors keep their profile. */
|
|
24
|
+
export function appDataDir({ platform = process.platform, home = os.homedir(), env = process.env } = {}) {
|
|
25
|
+
if (platform === 'win32') return env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
26
|
+
if (platform === 'darwin') return path.join(home, 'Library', 'Application Support');
|
|
27
|
+
return env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extension hosts whose extension folders we scan for editor PLUGINS (Cline,
|
|
31
|
+
// Copilot). VS Code forks keep the same `<publisher>.<name>-<version>` layout.
|
|
32
|
+
const EXTENSION_ROOTS = ['.vscode', '.vscode-insiders', '.cursor', '.windsurf', '.vscode-oss'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The detection matrix. Each entry lists independent signals; ANY hit is a
|
|
36
|
+
* positive, because a user may have a CLI without the GUI or vice versa.
|
|
37
|
+
* - home: dot-dirs/files directly under the home directory
|
|
38
|
+
* - app: profile directory names under the platform's roaming-config root
|
|
39
|
+
* - ext: installed-extension id patterns (for plugin-shaped hosts)
|
|
40
|
+
* - env: environment variables an editor exports into its own terminal —
|
|
41
|
+
* the strongest signal available, since it means we are running
|
|
42
|
+
* INSIDE that editor right now
|
|
43
|
+
*/
|
|
44
|
+
const EDITORS = [
|
|
45
|
+
{ id: 'claude-code', name: 'Claude Code', home: ['.claude'], env: ['CLAUDE_CODE_SESSION_ID', 'CLAUDE_PID'] },
|
|
46
|
+
{ id: 'cursor', name: 'Cursor', home: ['.cursor'], app: ['Cursor'], env: ['CURSOR_SESSION_ID', 'CURSOR_TRACE_ID'] },
|
|
47
|
+
{ id: 'codex', name: 'Codex', home: ['.codex'], env: ['CODEX_THREAD_ID'] },
|
|
48
|
+
{ id: 'gemini-cli', name: 'Gemini CLI', home: ['.gemini'] },
|
|
49
|
+
{ id: 'antigravity', name: 'Antigravity', home: ['.antigravity'], app: ['Antigravity'] },
|
|
50
|
+
{ id: 'vscode', name: 'VS Code', home: ['.vscode', '.vscode-insiders'], app: ['Code', 'Code - Insiders', 'VSCodium'] },
|
|
51
|
+
{ id: 'windsurf', name: 'Windsurf', home: ['.windsurf'], app: ['Windsurf'], env: ['WINDSURF_SESSION_ID'] },
|
|
52
|
+
{ id: 'cline', name: 'Cline', ext: [/^saoudrizwan\.claude-dev/i], env: ['CLINE_SESSION_ID'] },
|
|
53
|
+
{ id: 'copilot', name: 'GitHub Copilot', ext: [/^github\.copilot/i] },
|
|
54
|
+
{ id: 'aider', name: 'Aider', home: ['.aider', '.aider.conf.yml', '.aider.model.settings.yml'] },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const exists = (p) => { try { return fs.existsSync(p); } catch { return false; } };
|
|
58
|
+
|
|
59
|
+
/** Installed extension ids across every VS Code-shaped host on this machine. */
|
|
60
|
+
function installedExtensions(home) {
|
|
61
|
+
const ids = [];
|
|
62
|
+
for (const root of EXTENSION_ROOTS) {
|
|
63
|
+
const dir = path.join(home, root, 'extensions');
|
|
64
|
+
let entries;
|
|
65
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
if (!e.isDirectory()) continue;
|
|
68
|
+
ids.push({ id: e.name, from: `${root}/extensions/${e.name}` });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return ids;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detect the agent tools present on this machine.
|
|
76
|
+
* @param {{ home?: string, platform?: string, env?: object }} [opts]
|
|
77
|
+
* @returns {{ present: Map<string,{id,name,why}>, absent: Array<{id,name}>, all: Array }}
|
|
78
|
+
*/
|
|
79
|
+
export function detectEditors(opts = {}) {
|
|
80
|
+
const home = opts.home || os.homedir();
|
|
81
|
+
const platform = opts.platform || process.platform;
|
|
82
|
+
const env = opts.env || process.env;
|
|
83
|
+
const appRoot = appDataDir({ platform, home, env });
|
|
84
|
+
|
|
85
|
+
// Scanned once — readdir on up to five extension roots is the only
|
|
86
|
+
// non-trivial cost in this module, and most machines have one or two.
|
|
87
|
+
let extensions = null;
|
|
88
|
+
const getExtensions = () => (extensions ??= installedExtensions(home));
|
|
89
|
+
|
|
90
|
+
const present = new Map();
|
|
91
|
+
const absent = [];
|
|
92
|
+
|
|
93
|
+
for (const ed of EDITORS) {
|
|
94
|
+
let why = null;
|
|
95
|
+
|
|
96
|
+
for (const key of ed.env || []) {
|
|
97
|
+
if (env[key]) { why = `running inside it (${key})`; break; }
|
|
98
|
+
}
|
|
99
|
+
if (!why) for (const dir of ed.home || []) {
|
|
100
|
+
if (exists(path.join(home, dir))) { why = `~/${dir}`; break; }
|
|
101
|
+
}
|
|
102
|
+
if (!why) for (const dir of ed.app || []) {
|
|
103
|
+
if (exists(path.join(appRoot, dir))) { why = `${path.basename(appRoot)}/${dir}`; break; }
|
|
104
|
+
}
|
|
105
|
+
if (!why && ed.ext) {
|
|
106
|
+
const hit = getExtensions().find((x) => ed.ext.some((re) => re.test(x.id)));
|
|
107
|
+
if (hit) why = hit.from;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (why) present.set(ed.id, { id: ed.id, name: ed.name, why });
|
|
111
|
+
else absent.push({ id: ed.id, name: ed.name });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { present, absent, all: EDITORS.map((e) => ({ id: e.id, name: e.name })) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { EDITORS };
|