klypix-mcp 1.88.0 → 1.89.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/FORMAT.md +47 -0
- package/README.md +27 -7
- package/bin/klypix-brain-deleted.mjs +66 -11
- package/bin/klypix-brain-history.mjs +60 -6
- package/bin/klypix-git-tools.mjs +44 -12
- package/bin/klypix-install.mjs +8 -1
- package/bin/klypix-mcp.mjs +1 -1
- package/bin/klypix-worker.mjs +1 -1
- package/package.json +2 -2
- package/src/brain-doctor.mjs +59 -1
- package/src/brain-graveyard.mjs +148 -67
- package/src/brain-history.mjs +97 -13
- package/src/klypix-format.mjs +347 -16
- package/src/klypix-merge-driver.mjs +87 -12
- package/src/merge-brains.mjs +1834 -163
- package/src/setup.mjs +1 -1
package/FORMAT.md
CHANGED
|
@@ -17,6 +17,9 @@ your.klypix (a ZIP archive)
|
|
|
17
17
|
├── canvas.json spatial layout: order, positions, connections, lines, strokes, view, settings
|
|
18
18
|
├── items/
|
|
19
19
|
│ └── <shard>/<id>.json one file per item (content only; geometry lives in canvas.json)
|
|
20
|
+
├── graveyard.json Deleted cards index — absent until the first delete (see below)
|
|
21
|
+
├── graveyard/
|
|
22
|
+
│ └── <shard>/<id>.json a deleted card's item JSON, or a receipt placeholder
|
|
20
23
|
└── assets/ embedded binaries — any non-directory entry here is an asset
|
|
21
24
|
├── images/<shard>/<sha256>.<ext>
|
|
22
25
|
├── files/<shard>/<sha256>.bin
|
|
@@ -198,6 +201,50 @@ instead of an asset. New writers use `assetId` and leave `src` empty.
|
|
|
198
201
|
files — cards and arrows. Embedding binaries is done by the KLYPIX app when you drop
|
|
199
202
|
a file onto a canvas.
|
|
200
203
|
|
|
204
|
+
## Deleted cards — `graveyard.json` + `graveyard/`
|
|
205
|
+
|
|
206
|
+
A card deleted from a brain is moved, not destroyed. Its item file goes to
|
|
207
|
+
`graveyard/<shard>/<id>.json` (same sharding as `items/`), and an entry for it goes
|
|
208
|
+
into `graveyard.json`:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{ "version": 1, "entries": {
|
|
212
|
+
"txt_hto5hb3r_0_0": {
|
|
213
|
+
"rid": "r_3f1c…",
|
|
214
|
+
"deletedAt": 1790676275950,
|
|
215
|
+
"deletedBy": "user",
|
|
216
|
+
"deletion": { "initiator": "user", "cause": "history-restore", "source": "klypix-mcp", "confidence": "explicit" },
|
|
217
|
+
"area": "Work", "parentId": "ctn_…", "pos": { "x": 96, "y": 140, "w": 280, "h": 47 },
|
|
218
|
+
"preview": "first words of the card…"
|
|
219
|
+
} } }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Nothing under `graveyard/` is reachable from `canvas.json`'s `order`, so a deleted card
|
|
223
|
+
never renders and is never searched or counted, and a card is never live and in the
|
|
224
|
+
bin at once. Writers add fields to an entry, never rename them; `deletedBy` is the flat
|
|
225
|
+
copy of `deletion.initiator` kept for older readers.
|
|
226
|
+
|
|
227
|
+
An entry is one of three kinds:
|
|
228
|
+
|
|
229
|
+
| Kind | How to recognise it | Body file |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| deleted card | no `purged` | the card's item JSON, verbatim — restorable |
|
|
232
|
+
| permanently deleted | `purged: true`, no `restoredAs` | the placeholder `{"type":"text","content":"","purged":true}` |
|
|
233
|
+
| restored | `purged: true` and `restoredAs: "<id>"` | the same placeholder; the card lives on at `restoredAs` |
|
|
234
|
+
|
|
235
|
+
The two receipts carry no content, and they are the reason **the bin must travel with
|
|
236
|
+
the file**. A tool that rewrites a `.klypix` has to carry `graveyard.json` and
|
|
237
|
+
`graveyard/` through, or merge them. Dropping them makes the next merge with an older
|
|
238
|
+
copy of the brain read the deletion as "never happened" and bring the card back — a
|
|
239
|
+
permanently deleted one included.
|
|
240
|
+
|
|
241
|
+
Two id rules follow from the same idea. A card brought back, from the bin or from a
|
|
242
|
+
restore point, should get a new id derived from its deletion (`<id>__r_<12 hex>`, as
|
|
243
|
+
this package's restores do), never the id it was deleted with: other copies still hold
|
|
244
|
+
that deletion and would delete it again. And a card that Arrange collapses as a
|
|
245
|
+
duplicate is buried like any other deletion, with `mergedInto` naming the card that
|
|
246
|
+
absorbed it.
|
|
247
|
+
|
|
201
248
|
## Connections, links, tags
|
|
202
249
|
|
|
203
250
|
- **Arrows:** `canvas.json.connections` (`fromId`/`toId`, optional
|
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ npx klypix-mcp install
|
|
|
133
133
|
|
|
134
134
|
One command for supported editors detected on this machine. It finds the project root (walking up,
|
|
135
135
|
so running it from `src/` is fine), gives the project a brain if it doesn't have one, wires the
|
|
136
|
-
agent tools you actually have installed, registers the
|
|
136
|
+
agent tools you actually have installed, registers the card-level `.klypix` merge driver if it's a
|
|
137
137
|
git repo, and then **proves the result** before it exits:
|
|
138
138
|
|
|
139
139
|
```text
|
|
@@ -141,7 +141,7 @@ git repo, and then **proves the result** before it exits:
|
|
|
141
141
|
brain created brain.klypix — a starter brain, ready for its first decision
|
|
142
142
|
editors Claude Code · Cursor · Codex · Gemini CLI · Antigravity · VS Code
|
|
143
143
|
wired 9 file(s) · 9 updated (skipped 5 for tools you don't have)
|
|
144
|
-
git
|
|
144
|
+
git .klypix merge driver registered
|
|
145
145
|
verified ✓ 22 tools reachable via .mcp.json (892ms)
|
|
146
146
|
```
|
|
147
147
|
|
|
@@ -541,9 +541,18 @@ That registers a merge driver for `*.klypix` (a per-machine git config line plus
|
|
|
541
541
|
rule you commit) and provisions the engine it needs. When two people change the brain and one
|
|
542
542
|
pulls, git calls the engine instead of stopping: new cards from both sides are kept, a card only
|
|
543
543
|
one side edited takes that edit, and a card edited differently on both sides keeps **both**
|
|
544
|
-
versions — the second as a linked twin, never a silent overwrite.
|
|
545
|
-
|
|
546
|
-
|
|
544
|
+
versions — the second as a linked twin, never a silent overwrite. (One exception to "takes that
|
|
545
|
+
edit": a card is never changed to mean exactly what its own conflict twin beside it already means
|
|
546
|
+
(the whole card, not only its words) — both versions stay as they are and the driver's summary line
|
|
547
|
+
says so. Delete the one you do not want.) Deletions travel too: a deleted
|
|
548
|
+
card leaves a receipt in the brain's Deleted cards, and the driver merges those three-way, so a
|
|
549
|
+
card deleted — or permanently deleted — on one branch stays out instead of coming back from the
|
|
550
|
+
other, unless the other branch edited it. That edit comes back as a new card when the deleting
|
|
551
|
+
branch recorded the delete (a receipt in its Deleted cards); when it did not, the edited card
|
|
552
|
+
simply stays. A permanently deleted card stays out even then: the driver's summary line counts the
|
|
553
|
+
edits it dropped, and they remain in that branch's git history. Before returning, the merge asserts
|
|
554
|
+
it still contains every surviving card from both sides and refuses rather than hand back a result
|
|
555
|
+
that lost one.
|
|
547
556
|
|
|
548
557
|
The honest boundary: a machine that has not run `git-driver install` simply gets the old binary
|
|
549
558
|
conflict — safe degradation, not corruption — and git keeps both parents of every merge, so even a
|
|
@@ -570,7 +579,12 @@ was judged worse — but it is a real limit, not a guarantee.
|
|
|
570
579
|
|
|
571
580
|
### Restore points
|
|
572
581
|
|
|
573
|
-
Merging, tidying and gardening are
|
|
582
|
+
Merging, tidying and gardening are built to keep every card nobody deleted (one deliberate exception: a
|
|
583
|
+
card deleted permanently also leaves the other copies of the brain when they sync, an edited copy
|
|
584
|
+
included, and the merge reports that edit). Deleting permanently is not a guarantee that the text is
|
|
585
|
+
gone everywhere: a copy of the card that someone restored on another machine at about the same time,
|
|
586
|
+
or resized or edited after restoring it, can stay there and has to be deleted there too; and restore
|
|
587
|
+
points and git history keep what they already held. What none of them can undo is a
|
|
574
588
|
*deliberate-looking* deletion: you select a dozen cards, delete them, and save. That is not a bug
|
|
575
589
|
to prevent — a brain has to stay correctable, and an uncorrectable memory is worse than none — but
|
|
576
590
|
it deserves a way back, because the brain is **co-owned**: hooks, the MCP server, commit capture
|
|
@@ -584,6 +598,12 @@ npx klypix-mcp brain-history list # age, card count, delta against the
|
|
|
584
598
|
npx klypix-mcp brain-history restore <id> # and this is itself undoable
|
|
585
599
|
```
|
|
586
600
|
|
|
601
|
+
A restore is a merge into the brain as it is now, not a copy over the file, and it prints what it
|
|
602
|
+
changed. The point's cards come back — a card deleted since returns under a new id, so every other
|
|
603
|
+
copy of the brain agrees the old one was deleted — and cards written after the point move to
|
|
604
|
+
Deleted cards (`npx klypix-mcp brain-deleted list`), where each one can be restored. Permanently
|
|
605
|
+
deleted cards stay out unless you pass `--include-purged`.
|
|
606
|
+
|
|
587
607
|
They live under `~/.claude/project-brain/history/`, never beside the brain — nothing lands in git,
|
|
588
608
|
in the merge driver's path, or in your diffs, and they survive deletion of the `.klypix` file
|
|
589
609
|
itself. Routine writes are deduped and throttled to one a minute; a write that **removes cards** is
|
|
@@ -608,7 +628,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
608
628
|
| `npx klypix-mcp doctor` | One verdict: version, hosts, live sessions, tool count, drift. Exits non-zero — usable as a CI gate |
|
|
609
629
|
| `npx klypix-mcp runtime` | Passive per-connection process/RAM attribution (`--json`, optional `--watch seconds`); never kills or deduplicates |
|
|
610
630
|
| `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
|
|
611
|
-
| `npx klypix-mcp git-driver` | Register the
|
|
631
|
+
| `npx klypix-mcp git-driver` | Register the card-level `.klypix` merge driver for a repo (`status` to check) |
|
|
612
632
|
| `npx klypix-mcp git-hook` | Wire the agent-neutral commit-capture hook: rationale-bearing `feat`/`fix`/`perf` commits from any agent, branch, or worktree card into the brain at commit time (`install`/`remove`/`status`; sessions auto-install it where the hook slots are free) |
|
|
613
633
|
| `npx klypix-mcp brain-history` | Restore points for this brain — `list` them, `restore <id>` one. Written automatically before every brain write, kept machine-local, and never throttled away for a write that removes cards |
|
|
614
634
|
| `npx klypix-mcp diff [ref]` | Card-level brain diff against a git ref, as markdown |
|
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
// `klypix-mcp brain-deleted [list|restore <id…>|purge] [--brain <path>]`
|
|
3
3
|
// The recycle bin for a brain: cards a human deleted are kept recoverable
|
|
4
4
|
// instead of destroyed. Standalone: node bin/klypix-brain-deleted.mjs <args>
|
|
5
|
+
//
|
|
6
|
+
// Receipts (1.89): "Delete permanently" keeps a content-free receipt in the bin
|
|
7
|
+
// so every other copy of the brain drops the card too (see brain-graveyard.mjs).
|
|
8
|
+
// Receipts are not deleted cards — `list` hides them, restore refuses them, and
|
|
9
|
+
// `list <id>` names what happened to one.
|
|
10
|
+
//
|
|
11
|
+
// Every write re-reads the brain INSIDE the capture lock and refuses when the
|
|
12
|
+
// lock is held: reading outside it and writing later would silently roll back
|
|
13
|
+
// a capture (or a desktop save) that landed in between.
|
|
5
14
|
import fs from 'fs';
|
|
6
15
|
import path from 'path';
|
|
7
16
|
import { listGraveyard, purgeGraveyard, readGraveyardCard, restoreFromGraveyard, DEFAULT_RETENTION_DAYS } from '../src/brain-graveyard.mjs';
|
|
8
17
|
import { atomicWrite } from '../src/klypix-format.mjs';
|
|
18
|
+
import { brainCaptureLockPath, withAdvisoryWriteLock } from '../src/brain-write-lock.mjs';
|
|
19
|
+
import { snapshotBrain } from '../src/brain-history.mjs';
|
|
9
20
|
|
|
10
21
|
const argv = process.argv.slice(2).filter((a) => a !== 'brain-deleted');
|
|
11
22
|
const action = ['list', 'restore', 'purge'].includes(argv[0]) ? argv.shift() : 'list';
|
|
@@ -17,7 +28,6 @@ const all = argv.includes('--all');
|
|
|
17
28
|
const ids = argv.filter((a) => !a.startsWith('-'));
|
|
18
29
|
|
|
19
30
|
if (!fs.existsSync(brainPath)) { console.error(`No brain at ${brainPath}.`); process.exit(1); }
|
|
20
|
-
const buf = fs.readFileSync(brainPath);
|
|
21
31
|
|
|
22
32
|
const ago = (ts) => {
|
|
23
33
|
const m = Math.max(0, Math.round((Date.now() - Number(ts || 0)) / 60000));
|
|
@@ -27,8 +37,40 @@ const ago = (ts) => {
|
|
|
27
37
|
return h < 48 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
28
38
|
};
|
|
29
39
|
|
|
40
|
+
// Read-modify-write under the capture lock. `mutate(buf)` returns
|
|
41
|
+
// { buffer?, ...report }; no buffer means nothing to write.
|
|
42
|
+
async function lockedWrite(reason, mutate, { forceSnapshot = false } = {}) {
|
|
43
|
+
return withAdvisoryWriteLock(brainCaptureLockPath(brainPath), async (locked) => {
|
|
44
|
+
if (!locked) return { busy: true };
|
|
45
|
+
const report = await mutate(fs.readFileSync(brainPath));
|
|
46
|
+
if (!report.buffer) return report;
|
|
47
|
+
if (forceSnapshot) {
|
|
48
|
+
// The one irreversible action gets a restore point that no throttle may
|
|
49
|
+
// skip, taken inside the lock so it holds exactly the bytes replaced.
|
|
50
|
+
try { snapshotBrain(brainPath, { reason, force: true }); } catch { /* best-effort by contract */ }
|
|
51
|
+
await atomicWrite(brainPath, report.buffer, { reason, snapshot: false });
|
|
52
|
+
} else {
|
|
53
|
+
await atomicWrite(brainPath, report.buffer, { reason });
|
|
54
|
+
}
|
|
55
|
+
return report;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const refuseBusy = () => {
|
|
59
|
+
console.error('The brain is busy (another writer holds the capture lock) — retry in a moment. Nothing written.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
};
|
|
62
|
+
|
|
30
63
|
if (action === 'list') {
|
|
31
|
-
const
|
|
64
|
+
const buf = fs.readFileSync(brainPath);
|
|
65
|
+
const everything = await listGraveyard(buf, { receipts: 'include' });
|
|
66
|
+
// A receipt asked for by id: say what happened to it rather than "not found".
|
|
67
|
+
for (const e of everything) {
|
|
68
|
+
if (!ids.includes(e.id) || e.kind === 'deleted') continue;
|
|
69
|
+
console.log(e.kind === 'restored'
|
|
70
|
+
? ` ${e.id} restored as ${e.restoredAs} ${ago(e.restoredAt || e.deletedAt)} — it is live under that id`
|
|
71
|
+
: ` ${e.id} permanently deleted ${ago(e.purgedAt || e.deletedAt)} — its text is gone from this file (an image or file attached to it is not)`);
|
|
72
|
+
}
|
|
73
|
+
const entries = everything.filter((e) => e.kind === 'deleted');
|
|
32
74
|
if (!entries.length) {
|
|
33
75
|
console.log(`Nothing deleted from ${path.basename(brainPath)}.`);
|
|
34
76
|
console.log('Cards you delete from a brain are kept here, recoverable, instead of destroyed.');
|
|
@@ -53,15 +95,21 @@ if (action === 'list') {
|
|
|
53
95
|
|
|
54
96
|
if (action === 'restore') {
|
|
55
97
|
if (!ids.length) { console.error('Usage: brain-deleted restore <id…> (ids from `brain-deleted list`)'); process.exit(2); }
|
|
56
|
-
const res = await
|
|
98
|
+
const res = await lockedWrite('graveyard-restore', async (buf) => {
|
|
99
|
+
const r = await restoreFromGraveyard(buf, ids);
|
|
100
|
+
return r.restored.length ? r : { restored: [], skipped: r.skipped };
|
|
101
|
+
});
|
|
102
|
+
if (res.busy) refuseBusy();
|
|
57
103
|
if (!res.restored.length) {
|
|
58
104
|
for (const s of res.skipped) console.error(` ${s.id}: ${s.reason}`);
|
|
59
105
|
console.error('Nothing restored.');
|
|
60
106
|
process.exit(1);
|
|
61
107
|
}
|
|
62
|
-
await atomicWrite(brainPath, res.buffer, { reason: 'graveyard-restore' });
|
|
63
108
|
for (const r of res.restored) {
|
|
64
|
-
|
|
109
|
+
const as = r.restoredAs && r.restoredAs !== r.id ? ` as ${r.restoredAs}` : '';
|
|
110
|
+
console.log(r.already
|
|
111
|
+
? `${r.id} is already back${as}`
|
|
112
|
+
: `Restored ${r.id}${as}${r.reparented ? ' (its container is gone — placed at the canvas root)' : ''}`);
|
|
65
113
|
}
|
|
66
114
|
for (const s of res.skipped) console.log(`Skipped ${s.id}: ${s.reason}`);
|
|
67
115
|
console.log('If the app has this brain OPEN, close and reopen the tab so it sees the restored card.');
|
|
@@ -76,12 +124,19 @@ if (!ids.length && !all && !olderThan) {
|
|
|
76
124
|
}
|
|
77
125
|
const days = olderThan ? Number(String(olderThan).replace(/d$/i, '')) : null;
|
|
78
126
|
if (olderThan && !Number.isFinite(days)) { console.error(`--older-than expects days, e.g. --older-than ${DEFAULT_RETENTION_DAYS}d`); process.exit(2); }
|
|
79
|
-
const res = await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
127
|
+
const res = await lockedWrite('graveyard-purge', async (buf) => {
|
|
128
|
+
const r = await purgeGraveyard(buf, {
|
|
129
|
+
// --all means every deleted card; receipts hold nothing left to purge.
|
|
130
|
+
ids: ids.length ? ids : (all ? (await listGraveyard(buf, { receipts: 'hide' })).map((e) => e.id) : null),
|
|
131
|
+
olderThanDays: ids.length || all ? null : days,
|
|
132
|
+
});
|
|
133
|
+
return r.purged.length ? r : { purged: [] };
|
|
134
|
+
}, { forceSnapshot: true });
|
|
135
|
+
if (res.busy) refuseBusy();
|
|
83
136
|
if (!res.purged.length) { console.log('Nothing matched — nothing purged.'); process.exit(0); }
|
|
84
|
-
await atomicWrite(brainPath, res.buffer, { reason: 'graveyard-purge' });
|
|
85
137
|
console.log(`Purged ${res.purged.length} deleted card(s) permanently from ${path.basename(brainPath)}.`);
|
|
138
|
+
console.log('The delete itself is kept as a receipt with no content, so copies that sync with this version or later drop the card too.');
|
|
139
|
+
console.log('An edit of it that a merge kept as a separate card elsewhere is not removed: purge that card as well.');
|
|
140
|
+
console.log('Not its attachments: an image or file a purged card held stays in the brain\'s assets, in this file and every copy.');
|
|
86
141
|
console.log('Note: this removes them from the file, not from git history — a secret committed earlier is still in past commits.');
|
|
87
|
-
console.log(`The pre-purge state is a restore point: npx klypix-mcp brain-history list --brain "${brainPath}"`);
|
|
142
|
+
console.log(`The pre-purge state is a restore point, and the only undo: npx klypix-mcp brain-history list --brain "${brainPath}"`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `klypix-mcp brain-history [list|restore <id
|
|
2
|
+
// `klypix-mcp brain-history [list|restore <id> [--include-purged]|prune] [--brain <path>]`
|
|
3
3
|
// The human surface for brain restore points. Protection nobody can see is
|
|
4
4
|
// protection nobody trusts, so `list` is the default and it says plainly what
|
|
5
|
-
// each point would give back.
|
|
5
|
+
// each point would give back, and `restore` says what it changed.
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { listBrainHistory, pruneBrainHistory, restoreBrainSnapshot, historyDirFor } from '../src/brain-history.mjs';
|
|
@@ -11,6 +11,7 @@ const argv = process.argv.slice(2).filter((a) => a !== 'brain-history');
|
|
|
11
11
|
const action = ['list', 'restore', 'prune'].includes(argv[0]) ? argv.shift() : 'list';
|
|
12
12
|
const brainIdx = argv.indexOf('--brain');
|
|
13
13
|
const brainPath = path.resolve(brainIdx >= 0 && argv[brainIdx + 1] ? argv.splice(brainIdx, 2)[1] : 'brain.klypix');
|
|
14
|
+
const includePurged = argv.includes('--include-purged');
|
|
14
15
|
const positional = argv.filter((a) => !a.startsWith('-'));
|
|
15
16
|
|
|
16
17
|
const ago = (ts) => {
|
|
@@ -50,7 +51,8 @@ if (action === 'list') {
|
|
|
50
51
|
console.log(` ${e.id} ${ago(e.ts).padEnd(9)} ${String(cards ?? '?').padStart(5)} cards ${kb(e.bytes).padStart(8)}${e.reason ? ` [${e.reason}]` : ''}${deltaText}`);
|
|
51
52
|
}
|
|
52
53
|
console.log(`\nRestore: npx klypix-mcp brain-history restore <id> --brain "${brainPath}"`);
|
|
53
|
-
console.log('
|
|
54
|
+
console.log("A restore brings that point's cards back and moves cards added since to Deleted cards.");
|
|
55
|
+
console.log('It snapshots the current file first, so it is itself undoable.');
|
|
54
56
|
process.exit(0);
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -63,13 +65,65 @@ if (action === 'prune') {
|
|
|
63
65
|
// restore
|
|
64
66
|
const id = positional[0];
|
|
65
67
|
if (!id) {
|
|
66
|
-
console.error('Usage: npx klypix-mcp brain-history restore <id> [--brain <path>]');
|
|
68
|
+
console.error('Usage: npx klypix-mcp brain-history restore <id> [--include-purged] [--brain <path>]');
|
|
67
69
|
console.error('Run `npx klypix-mcp brain-history list` to see the ids.');
|
|
68
70
|
process.exit(2);
|
|
69
71
|
}
|
|
70
72
|
const { parseKlypix } = await import('../src/klypix-format.mjs');
|
|
71
|
-
const res = await restoreBrainSnapshot(brainPath, id, { parse: parseKlypix });
|
|
73
|
+
const res = await restoreBrainSnapshot(brainPath, id, { parse: parseKlypix, includePurged });
|
|
72
74
|
if (!res.ok) { console.error(`Restore failed: ${res.error}`); process.exit(1); }
|
|
73
|
-
|
|
75
|
+
|
|
76
|
+
const SHOW = 10;
|
|
77
|
+
const listIds = (ids, fmt = (x) => x) => {
|
|
78
|
+
for (const x of ids.slice(0, SHOW)) console.log(` ${fmt(x)}`);
|
|
79
|
+
if (ids.length > SHOW) console.log(` …and ${ids.length - SHOW} more`);
|
|
80
|
+
};
|
|
81
|
+
if (res.mode === 'merge') {
|
|
82
|
+
const { reverted = [], restored = [], revived = [], buried = [], keptPurged = [] } = res;
|
|
83
|
+
// Ids alone mean nothing to a person: show each card's first words, from the
|
|
84
|
+
// live card or from its Deleted cards entry.
|
|
85
|
+
const text = new Map();
|
|
86
|
+
try {
|
|
87
|
+
const { struct } = await parseKlypix(fs.readFileSync(brainPath));
|
|
88
|
+
for (const c of struct.cards || []) text.set(c.id, String(c.text || c.title || ''));
|
|
89
|
+
for (const g of struct.graveyard || []) if (!text.has(g.id)) text.set(g.id, String(g.preview || ''));
|
|
90
|
+
} catch { /* ids only */ }
|
|
91
|
+
const said = (id) => {
|
|
92
|
+
const s = (text.get(id) || '').replace(/\s+/g, ' ').trim();
|
|
93
|
+
return s ? `${id} "${s.length > 60 ? `${s.slice(0, 59)}…` : s}"` : id;
|
|
94
|
+
};
|
|
95
|
+
console.log(`Restored ${brainPath} from ${res.restoredFrom} (${kb(res.bytes)}), merged into the brain as it is now:`);
|
|
96
|
+
if (reverted.length) {
|
|
97
|
+
console.log(` ${reverted.length} card(s) set back to how they were then:`);
|
|
98
|
+
listIds(reverted, said);
|
|
99
|
+
}
|
|
100
|
+
if (restored.length) {
|
|
101
|
+
console.log(` ${restored.length} card(s) that had left with no record came back:`);
|
|
102
|
+
listIds(restored, said);
|
|
103
|
+
}
|
|
104
|
+
if (revived.length) {
|
|
105
|
+
console.log(` ${revived.length} card(s) back from Deleted cards, under new ids so every copy agrees the old ones were deleted:`);
|
|
106
|
+
listIds(revived, (r) => (r.already ? `${said(r.as)} (already back)` : said(r.as)));
|
|
107
|
+
}
|
|
108
|
+
if (buried.length) {
|
|
109
|
+
console.log(` ${buried.length} card(s) added since that point moved to Deleted cards:`);
|
|
110
|
+
listIds(buried, said);
|
|
111
|
+
console.log(` Bring one back: npx klypix-mcp brain-deleted restore <id> --brain "${brainPath}"`);
|
|
112
|
+
}
|
|
113
|
+
if (keptPurged.length) {
|
|
114
|
+
console.log(` ${keptPurged.length} permanently deleted card(s) kept out:`);
|
|
115
|
+
listIds(keptPurged);
|
|
116
|
+
console.log(' Add --include-purged to bring them back too (under new ids).');
|
|
117
|
+
}
|
|
118
|
+
if (!reverted.length && !restored.length && !buried.length && !keptPurged.length && revived.every((r) => r.already)) {
|
|
119
|
+
console.log(' The brain already matched that point; nothing changed.');
|
|
120
|
+
}
|
|
121
|
+
} else if (res.mode === 'recreate') {
|
|
122
|
+
console.log(`The brain file was missing. Recreated ${brainPath} from ${res.restoredFrom} (${kb(res.bytes)}).`);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`Restored ${brainPath} from ${res.restoredFrom} (${kb(res.bytes)}).`);
|
|
125
|
+
console.warn('Warning: the merge engine installed beside this command is older, so this restore replaced the whole file.');
|
|
126
|
+
console.warn('Brain Sync and git will read the cards it removed as deletes. Update with: npx klypix-mcp install');
|
|
127
|
+
}
|
|
74
128
|
if (res.safetyId) console.log(`The state you just replaced was saved as ${res.safetyId} — undo with: npx klypix-mcp brain-history restore ${res.safetyId}`);
|
|
75
129
|
console.log('If the app has this brain OPEN, close and reopen the tab: its in-memory copy is now older than disk and a save would merge it back.');
|
package/bin/klypix-git-tools.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// klypix-git-tools — the GitHub lane: three verbs that put the brain where
|
|
3
3
|
// dev teams actually live (the repo and the PR page).
|
|
4
4
|
//
|
|
5
|
-
// git-driver [install|status] [repo] register the
|
|
5
|
+
// git-driver [install|status] [repo] register the card-level .klypix merge
|
|
6
6
|
// driver for a repo, zero-command
|
|
7
7
|
// diff [ref] [--brain <path>] readable brain diff vs a git ref
|
|
8
8
|
// pr-brief [baseRef] [--brain <path>] brain cards touching the files
|
|
@@ -85,21 +85,51 @@ async function loadEngine() {
|
|
|
85
85
|
|
|
86
86
|
// ── git-driver ──────────────────────────────────────────────────────────────
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Dependencies first: klypix-format ← brain-graveyard ← merge-brains ← driver.
|
|
89
|
+
// Each file is swapped in atomically, so at every moment the set on disk can
|
|
90
|
+
// link — a new merge-brains beside an old klypix-format is the one mix that
|
|
91
|
+
// cannot (it imports names the old file lacks).
|
|
92
|
+
const ENGINE_FILES = ['klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs'];
|
|
89
93
|
const ENGINE_DEPS = ['jszip', 'fractional-indexing'];
|
|
90
94
|
|
|
95
|
+
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
|
|
96
|
+
|
|
91
97
|
// Make sure the INSTALLED runtime can actually run the driver: the four
|
|
92
98
|
// engine files plus their two runtime deps. This is deliberately a
|
|
93
99
|
// light provision — it never touches hooks or servers; the full installer
|
|
94
100
|
// remains `npx klypix-mcp install`.
|
|
95
|
-
|
|
101
|
+
//
|
|
102
|
+
// Never a downgrade. The same files arrive from the full installer, the
|
|
103
|
+
// desktop bundle and auto-update; an older `npx klypix-mcp git-driver install`
|
|
104
|
+
// (or a pinned devDependency) must not put its engine under a newer one. So
|
|
105
|
+
// the files are copied only when the install gate says this package may
|
|
106
|
+
// install (brainInstallDecision — the same rule the full installer uses), and
|
|
107
|
+
// then always as the whole set.
|
|
108
|
+
async function ensureDriverRuntime() {
|
|
96
109
|
const provisioned = [];
|
|
97
110
|
fs.mkdirSync(BRAIN_DIR, { recursive: true });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
const { brainInstallDecision } = await import(new URL('../src/install-version.mjs', import.meta.url).href);
|
|
112
|
+
const pkg = readJson(path.join(HERE, '..', 'package.json'));
|
|
113
|
+
const stamp = readJson(path.join(BRAIN_DIR, '.brain-version.json'));
|
|
114
|
+
const runtime = readJson(path.join(BRAIN_DIR, '.mcp-runtime.json'));
|
|
115
|
+
const decision = brainInstallDecision({ candidateVersion: pkg?.version, stamp, runtime });
|
|
116
|
+
const result = { provisioned };
|
|
117
|
+
if (decision.action === 'preserve') {
|
|
118
|
+
const owner = decision.reason === 'dev-owned' ? 'a dev deploy' : `v${decision.installedVersion}${stamp?.via ? ` (via ${stamp.via})` : ''}`;
|
|
119
|
+
result.kept = `kept the engine installed by ${owner}; this package is v${decision.candidateVersion}`;
|
|
120
|
+
const missing = ENGINE_FILES.filter((f) => !fs.existsSync(path.join(BRAIN_DIR, f)));
|
|
121
|
+
if (missing.length) result.missing = missing;
|
|
122
|
+
} else {
|
|
123
|
+
// Only files that differ are written, so what is on disk afterwards is
|
|
124
|
+
// exactly this package's set.
|
|
125
|
+
for (const f of ENGINE_FILES) {
|
|
126
|
+
const dest = path.join(BRAIN_DIR, f);
|
|
127
|
+
const body = fs.readFileSync(path.join(SRC, f), 'utf8');
|
|
128
|
+
if (fs.existsSync(dest) && fs.readFileSync(dest, 'utf8') === body) continue;
|
|
129
|
+
const tmp = `${dest}.klypix-new-${process.pid}`;
|
|
130
|
+
fs.writeFileSync(tmp, body);
|
|
131
|
+
try { fs.renameSync(tmp, dest); }
|
|
132
|
+
catch (err) { try { fs.unlinkSync(tmp); } catch { /* */ } throw err; }
|
|
103
133
|
provisioned.push(f);
|
|
104
134
|
}
|
|
105
135
|
}
|
|
@@ -152,7 +182,7 @@ function ensureDriverRuntime() {
|
|
|
152
182
|
};
|
|
153
183
|
const seen = new Set();
|
|
154
184
|
for (const dep of ENGINE_DEPS) provisionDep(dep, path.join(HERE, '..'), seen);
|
|
155
|
-
return
|
|
185
|
+
return result;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
const DRIVER_ATTR_RULE = '*.klypix merge=klypix -text';
|
|
@@ -180,21 +210,23 @@ async function gitDriver() {
|
|
|
180
210
|
process.exit(configured && gaHasRule && runtimeOk ? 0 : 1);
|
|
181
211
|
}
|
|
182
212
|
|
|
183
|
-
const provisioned = ensureDriverRuntime();
|
|
213
|
+
const { provisioned, kept, missing } = await ensureDriverRuntime();
|
|
184
214
|
let already = false;
|
|
185
215
|
try { already = (await gitText(toplevel, 'config', '--get', 'merge.klypix.driver')) === driverCmd; } catch { /* unset */ }
|
|
186
216
|
if (!already) {
|
|
187
|
-
await git(toplevel, ['config', 'merge.klypix.name', 'KLYPIX
|
|
217
|
+
await git(toplevel, ['config', 'merge.klypix.name', 'KLYPIX brain merge (3-way, card by card)']);
|
|
188
218
|
await git(toplevel, ['config', 'merge.klypix.driver', driverCmd]);
|
|
189
219
|
}
|
|
190
220
|
let gaState = 'present';
|
|
191
221
|
if (!gaHasRule) {
|
|
192
|
-
const rule = `${gaText && !gaText.endsWith('\n') ? '\n' : ''}# .klypix brains merge
|
|
222
|
+
const rule = `${gaText && !gaText.endsWith('\n') ? '\n' : ''}# .klypix brains merge card by card via the KLYPIX 3-way driver\n# (per-machine registration: npx klypix-mcp git-driver install).\n${DRIVER_ATTR_RULE}\n`;
|
|
193
223
|
fs.appendFileSync(gaPath, rule);
|
|
194
224
|
gaState = 'added';
|
|
195
225
|
}
|
|
196
226
|
console.log(`✓ ${already ? 'Already registered' : 'Registered'} the .klypix merge driver for ${toplevel}`);
|
|
197
227
|
console.log(` driver: ${driverPath}${provisioned.length ? ` (provisioned: ${provisioned.join(', ')})` : ''}`);
|
|
228
|
+
if (kept) console.log(` engine: ${kept}`);
|
|
229
|
+
if (missing?.length) console.log(` ! the installed engine is missing ${missing.join(', ')} — repair it with: npx klypix-mcp@latest install`);
|
|
198
230
|
console.log(` .gitattributes rule: ${gaState}${gaState === 'added' ? ' — commit it so every teammate\'s clone routes .klypix merges here' : ''}`);
|
|
199
231
|
console.log(' Teammates run the same command once per machine; unregistered machines fall back to a normal conflict.');
|
|
200
232
|
}
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -395,7 +395,14 @@ try {
|
|
|
395
395
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
396
396
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
397
397
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
398
|
-
|
|
398
|
+
// Files are renamed one at a time in this order (the hook last), so the
|
|
399
|
+
// shared libraries go FIRST and the merge engine in dependency order —
|
|
400
|
+
// klypix-format, brain-graveyard, merge-brains, the git driver. A newer
|
|
401
|
+
// file beside an older klypix-format can fail to link; an older one beside
|
|
402
|
+
// a newer klypix-format cannot. merge-brains and the driver ship here so
|
|
403
|
+
// brain-history's restore merge, the KLYPIX core and the git driver all
|
|
404
|
+
// find one engine in this directory.
|
|
405
|
+
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
399
406
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
400
407
|
}
|
|
401
408
|
for (const [src, dst] of [
|
package/bin/klypix-mcp.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const USAGE = [
|
|
|
35
35
|
' init seed a starter ./brain.klypix + print an MCP config',
|
|
36
36
|
' garden-code [brain] print the human approval code for brain_garden',
|
|
37
37
|
' uninstall [--check|--yes|unlink] remove this install from the machine (--check inventories first; never deletes a .klypix)',
|
|
38
|
-
' git-driver [install|status] [repo] register the
|
|
38
|
+
' git-driver [install|status] [repo] register the card-level .klypix merge driver for a repo (zero-command teams)',
|
|
39
39
|
' git-hook [install|remove|status] wire the agent-neutral commit-capture hook (any agent/branch/worktree → brain cards)',
|
|
40
40
|
' brain-history [list|restore <id>] restore points for this brain — undo an accidental delete, edit, or overwrite',
|
|
41
41
|
' brain-deleted [list|restore|purge] recycle bin for this brain — cards you deleted, kept recoverable',
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -116,7 +116,7 @@ await runVerb('doctor', './klypix-doctor.mjs');
|
|
|
116
116
|
await runVerb('conformance', './klypix-conformance.mjs');
|
|
117
117
|
|
|
118
118
|
// `npx klypix-mcp git-driver | diff | pr-brief` — the GitHub lane: register the
|
|
119
|
-
//
|
|
119
|
+
// card-level .klypix merge driver for any repo, render a readable brain diff vs a
|
|
120
120
|
// git ref, and print the brain cards touching a PR's changed files. One module,
|
|
121
121
|
// three verbs (it reads argv[2] itself).
|
|
122
122
|
await runVerb('git-driver', './klypix-git-driver.mjs');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.89.0",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
5
|
"description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
|
|
6
6
|
"type": "module",
|
|
@@ -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/npx-owned-names.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/session-mailbox.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/project-binding-rebind.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/capture-gap.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/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.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/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.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/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.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-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.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/canvas-groups.mjs && node test/git-tools.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs",
|
|
87
|
+
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.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/session-mailbox.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/project-binding-rebind.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/capture-gap.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/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.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/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/arrange-receipts.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/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.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-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/revival-map.mjs && node test/merge-scale.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/canvas-groups.mjs && node test/git-tools.mjs && node test/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.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",
|