klypix-mcp 1.48.0 → 1.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -8
- package/package.json +1 -1
- package/src/klypix-merge-driver.mjs +5 -2
- package/src/merge-brains.mjs +58 -7
package/README.md
CHANGED
|
@@ -292,12 +292,43 @@ Apache-2.0 and work with no app installed. The app's interface is available in E
|
|
|
292
292
|
|
|
293
293
|
## Git and concurrency
|
|
294
294
|
|
|
295
|
-
One file in your repo, committed with your code — versioned, branchable, portable.
|
|
295
|
+
One file in your repo, committed with your code — versioned, branchable, portable. So two
|
|
296
|
+
developers already share one brain the way they share code: clone, branch, pull.
|
|
296
297
|
|
|
297
|
-
Be precise about what git does
|
|
298
|
-
`Bin 1308328 -> 1309005 bytes
|
|
299
|
-
all-or-nothing take-ours or take-theirs
|
|
300
|
-
|
|
298
|
+
Be precise about what git does on its own: `brain.klypix` is a binary ZIP. Git shows
|
|
299
|
+
`Bin 1308328 -> 1309005 bytes` and produces zero line diffs, so out of the box a conflict on it is
|
|
300
|
+
an all-or-nothing take-ours or take-theirs, and a reviewer sees nothing. **Card-level merge safety
|
|
301
|
+
comes from the KLYPIX engine** — but since 1.48.0 you can hand that engine to git and read its
|
|
302
|
+
output in a PR:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npx klypix-mcp git-driver install # once per clone, in any repo
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
That registers a merge driver for `*.klypix` (a per-machine git config line plus a `.gitattributes`
|
|
309
|
+
rule you commit) and provisions the engine it needs. When two people change the brain and one
|
|
310
|
+
pulls, git calls the engine instead of stopping: new cards from both sides are kept, a card only
|
|
311
|
+
one side edited takes that edit, and a card edited differently on both sides keeps **both**
|
|
312
|
+
versions — the second as a linked twin, never a silent overwrite. Before returning, the merge
|
|
313
|
+
asserts it still contains every surviving card from both sides and refuses rather than hand back a
|
|
314
|
+
result that lost one.
|
|
315
|
+
|
|
316
|
+
The honest boundary: a machine that has not run `git-driver install` simply gets the old binary
|
|
317
|
+
conflict — safe degradation, not corruption — and git keeps both parents of every merge, so even a
|
|
318
|
+
merge you dislike is reconstructable. It is a merge *on pull*, not live sync.
|
|
319
|
+
|
|
320
|
+
For review, two commands turn a binary blob into something a human can read:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
npx klypix-mcp diff main # card-level: what was added / updated / removed
|
|
324
|
+
npx klypix-mcp pr-brief origin/main # the brain cards that reference this PR's changed files
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`diff` compares meaning rather than bytes (a re-save restamps timestamps; that is not a change).
|
|
328
|
+
`pr-brief` matches a card's `#file-…` evidence anchors against the changed paths, so a reviewer
|
|
329
|
+
sees the decisions already recorded about the code in front of them. `examples/github/brain-pr.yml`
|
|
330
|
+
wires both into a sticky pull-request comment using nothing but the checkout and the default
|
|
331
|
+
`GITHUB_TOKEN` — no KLYPIX service in the path.
|
|
301
332
|
|
|
302
333
|
Concurrent sessions serialize behind a capture lock, and each write is a temp file plus an atomic
|
|
303
334
|
rename, so a crash mid-write leaves the previous good file intact. The lock is advisory with a
|
|
@@ -307,6 +338,24 @@ was judged worse — but it is a real limit, not a guarantee.
|
|
|
307
338
|
|
|
308
339
|
---
|
|
309
340
|
|
|
341
|
+
## The command line
|
|
342
|
+
|
|
343
|
+
The MCP verbs below are what agents call. These are what **you** call:
|
|
344
|
+
|
|
345
|
+
| Command | What it does |
|
|
346
|
+
|---|---|
|
|
347
|
+
| `npx klypix-mcp init` | Seed a starter `brain.klypix` here and print an MCP config |
|
|
348
|
+
| `npx klypix-mcp install` | Install the engine + Claude Code hooks on this machine (see Quick start) |
|
|
349
|
+
| `npx klypix-mcp link` | Wire this project for Cursor, Cline, Windsurf, Copilot, Gemini CLI, Aider (`--check` audits) |
|
|
350
|
+
| `npx klypix-mcp doctor` | One verdict: version, hosts, live sessions, tool count, drift. Exits non-zero — usable as a CI gate |
|
|
351
|
+
| `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
|
|
352
|
+
| `npx klypix-mcp git-driver` | Register the lossless `.klypix` merge driver for a repo (`status` to check) |
|
|
353
|
+
| `npx klypix-mcp diff [ref]` | Card-level brain diff against a git ref, as markdown |
|
|
354
|
+
| `npx klypix-mcp pr-brief [ref]` | Brain cards referencing the files changed since a ref, as markdown |
|
|
355
|
+
| `npx klypix-mcp garden-code` | Print the human approval code `brain_garden` requires |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
310
359
|
## The 18 verbs
|
|
311
360
|
|
|
312
361
|
| Tool | What it does |
|
|
@@ -477,9 +526,9 @@ Read this section before you build on any of it.
|
|
|
477
526
|
Every number here is measured on our own project brain. Nothing below is published, benchmarked or
|
|
478
527
|
independently validated.
|
|
479
528
|
|
|
480
|
-
- **Dogfood scale.** KLYPIX itself is built with its own brain: **1,
|
|
529
|
+
- **Dogfood scale.** KLYPIX itself is built with its own brain: **1,645 cards and 1,521
|
|
481
530
|
connections**, written by multiple concurrent agent sessions, receipts in the file. Current as of
|
|
482
|
-
2026-
|
|
531
|
+
2026-08-01.
|
|
483
532
|
- **Recall.** 73% of past decisions recovered with one search round, 55% brief-only, 0% cold.
|
|
484
533
|
Caveat that travels with it: n=20, our own brain, self-authored questions, LLM-judged.
|
|
485
534
|
- **Ranker.** recall@5 of the true source card went **15% → 40%** across two upgrades (n=20 frozen
|
|
@@ -487,7 +536,7 @@ independently validated.
|
|
|
487
536
|
experiment that *regressed* — contextual prefixes on short cards — is recorded next to the wins.
|
|
488
537
|
- **What we do not publish.** No download count: this package's own 24-hour auto-updater generates
|
|
489
538
|
most of it, so it is not a user count. No adoption, team or customer figures. No brief-token
|
|
490
|
-
figure — the last one was measured at ~600 cards and is stale at 1,
|
|
539
|
+
figure — the last one was measured at ~600 cards and is stale at 1,645.
|
|
491
540
|
- **The eval harness is not in this repo.** It lives in the private KLYPIX desktop repository. The
|
|
492
541
|
numbers above are ours to defend, not yours to reproduce from here — treat them accordingly.
|
|
493
542
|
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// resolves to the edit — no-loss wins over delete).
|
|
32
32
|
|
|
33
33
|
import fs from 'node:fs';
|
|
34
|
-
import { mergeBrains } from './merge-brains.mjs';
|
|
34
|
+
import { mergeBrains, sameMeaning } from './merge-brains.mjs';
|
|
35
35
|
import { parseKlypix, shard } from './klypix-format.mjs';
|
|
36
36
|
|
|
37
37
|
// id -> verbatim item JSON string for one side (null for an empty/absent side).
|
|
@@ -68,8 +68,11 @@ try {
|
|
|
68
68
|
const inA = ai.has(id), inB = ti.has(id);
|
|
69
69
|
if (inA && inB) continue; // alive on both
|
|
70
70
|
if (!inA && !inB) { deletedIds.push(id); continue; } // deleted on both
|
|
71
|
+
// "Untouched" by MEANING, not bytes — a side that merely re-saved the
|
|
72
|
+
// file restamps volatile fields (updatedAt), and a byte compare would
|
|
73
|
+
// read that as an edit and silently refuse to propagate a real delete.
|
|
71
74
|
const survivorJson = inA ? ai.get(id) : ti.get(id);
|
|
72
|
-
if (survivorJson
|
|
75
|
+
if (sameMeaning(survivorJson, baseJson)) deletedIds.push(id); // delete vs untouched → honor
|
|
73
76
|
// delete vs EDIT → no tombstone; union keeps the edited card
|
|
74
77
|
}
|
|
75
78
|
}
|
package/src/merge-brains.mjs
CHANGED
|
@@ -40,9 +40,56 @@ const isValidZKey = (k) => { try { generateKeyBetween(k, null); return true; } c
|
|
|
40
40
|
const rand = () => Math.random().toString(36).slice(2, 10);
|
|
41
41
|
const ARCHIVE = /^archive$/i;
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
43
|
+
// ── Semantic item comparison (2026-08-01 field fix) ─────────────────────────
|
|
44
|
+
// A raw byte compare of item JSON was the change detector, on the assumption
|
|
45
|
+
// that "unchanged cards keep byte-identical JSON". That assumption DIED the
|
|
46
|
+
// day cards gained touch metadata: `updatedAt` is restamped whenever a card is
|
|
47
|
+
// written, so two sides holding the SAME card with the SAME text differ in
|
|
48
|
+
// bytes — and every first real sync spawned __agconf conflict twins for cards
|
|
49
|
+
// nobody edited (field-proven on the founder's pump-doctor brain: 5 twins,
|
|
50
|
+
// differing field list = ["updatedAt"] exactly).
|
|
51
|
+
//
|
|
52
|
+
// The fix is the same discipline the sync core and the brain diff already use:
|
|
53
|
+
// compare PARSED MEANING with volatile/derived fields stripped, key-sorted so
|
|
54
|
+
// two writers' key orders can't fake a difference. Byte-compare survives as the
|
|
55
|
+
// fallback for anything unparseable — a malformed item must never crash a merge.
|
|
56
|
+
//
|
|
57
|
+
// VOLATILE = written by the act of saving, not by a human/agent decision:
|
|
58
|
+
// updatedAt — touch timestamp zIndex — display order derived from zKey
|
|
59
|
+
// Everything else (content, colors, geometry, evidence, author…) stays load-
|
|
60
|
+
// bearing: a real edit to any of them is still a real conflict.
|
|
61
|
+
const VOLATILE_ITEM_FIELDS = ['updatedAt', 'zIndex'];
|
|
62
|
+
|
|
63
|
+
const sortedStable = (v) => JSON.stringify(v, (_k, val) =>
|
|
64
|
+
(val && typeof val === 'object' && !Array.isArray(val))
|
|
65
|
+
? Object.fromEntries(Object.keys(val).sort().map(k => [k, val[k]]))
|
|
66
|
+
: val);
|
|
67
|
+
|
|
68
|
+
function itemSignature(json) {
|
|
69
|
+
if (json == null) return null;
|
|
70
|
+
try {
|
|
71
|
+
const obj = JSON.parse(json);
|
|
72
|
+
for (const f of VOLATILE_ITEM_FIELDS) delete obj[f];
|
|
73
|
+
return sortedStable(obj);
|
|
74
|
+
} catch {
|
|
75
|
+
return json; // unparseable → byte identity, as before
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True when two item JSON strings mean the same thing (volatile fields aside).
|
|
80
|
+
* EXPORTED as the single definition of "did this card actually change" — the
|
|
81
|
+
* git merge driver and the Brain Sync core both decide committed-absence
|
|
82
|
+
* tombstones with it, so all three transports agree on what an edit is. */
|
|
83
|
+
export const sameMeaning = (a, b) => {
|
|
84
|
+
if (a === b) return true; // fast path: byte-identical
|
|
85
|
+
if (a == null || b == null) return false;
|
|
86
|
+
return itemSignature(a) === itemSignature(b);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Load one .klypix buffer into a flat, comparison-friendly shape. Item JSON is
|
|
90
|
+
// kept VERBATIM (the merge must write back exactly what a side held); whether
|
|
91
|
+
// two versions actually differ is decided by sameMeaning(), never by these
|
|
92
|
+
// bytes — see its note on volatile fields.
|
|
46
93
|
async function loadSide(buf) {
|
|
47
94
|
if (!buf) return null;
|
|
48
95
|
const { zip, canvas, manifest, struct } = await parseKlypix(buf);
|
|
@@ -128,9 +175,13 @@ export async function mergeBrains({ base = null, ours, theirs, deletedIds = [] }
|
|
|
128
175
|
// ── Choose CONTENT ──────────────────────────────────────────────────────
|
|
129
176
|
let json, side;
|
|
130
177
|
if (inO && inT) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
// Change + divergence are judged by MEANING, not bytes (see sameMeaning):
|
|
179
|
+
// a restamped `updatedAt` is not an edit, and two copies of one card that
|
|
180
|
+
// differ only in volatile fields are not in conflict.
|
|
181
|
+
const oChg = !inB || !sameMeaning(O.items[id], baseItem(id));
|
|
182
|
+
const tChg = !inB || !sameMeaning(T.items[id], baseItem(id));
|
|
183
|
+
const diverged = !sameMeaning(O.items[id], T.items[id]);
|
|
184
|
+
if (inB && oChg && tChg && diverged) {
|
|
134
185
|
// GENUINE content conflict: a card that EXISTED at open, edited differently
|
|
135
186
|
// on both sides → human stays live, agent version preserved as a twin.
|
|
136
187
|
json = O.items[id]; side = 'ours';
|
|
@@ -138,7 +189,7 @@ export async function mergeBrains({ base = null, ours, theirs, deletedIds = [] }
|
|
|
138
189
|
extras.push({ id: twinId, json: T.items[id], srcPos: T.positions[id] || O.positions[id], of: id });
|
|
139
190
|
conflicts.push({ id, kind: 'content', keptLive: 'ours', twin: twinId });
|
|
140
191
|
} else if (tChg && !oChg) { json = T.items[id]; side = 'theirs'; delta.updated.push(id); }
|
|
141
|
-
else if (!inB &&
|
|
192
|
+
else if (!inB && diverged) {
|
|
142
193
|
// Same NEW card (same id) present on BOTH sides but never in base — e.g. an
|
|
143
194
|
// agent card the app also holds via live-apply, re-serialized slightly
|
|
144
195
|
// differently. It's the SAME card, NOT a conflict → take the disk/agent
|