klypix-mcp 1.88.0 → 1.90.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 +88 -24
- package/bin/klypix-brain-deleted.mjs +66 -11
- package/bin/klypix-brain-history.mjs +60 -6
- package/bin/klypix-doctor.mjs +8 -3
- 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 +59 -7
- package/package.json +2 -2
- package/src/agent-presence.mjs +63 -20
- package/src/brain-doctor.mjs +872 -43
- package/src/brain-graveyard.mjs +148 -67
- package/src/brain-history.mjs +97 -13
- package/src/global-brain-hook.mjs +345 -73
- package/src/klypix-format.mjs +347 -16
- package/src/klypix-merge-driver.mjs +87 -12
- package/src/mcp-auto-update.mjs +1184 -173
- package/src/mcp-presence.mjs +1 -1
- package/src/mcp-supervisor.mjs +806 -93
- package/src/merge-brains.mjs +1834 -163
- package/src/runtime-inspector.mjs +83 -6
- 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
|
|
|
@@ -460,8 +460,8 @@ receiving model calls `brain_message_receipt` with the exact message id and per-
|
|
|
460
460
|
token; only that token-bound action records `consumed`. Pending, offered, and acknowledged notes
|
|
461
461
|
survive reconnects. Expiry or bounded-capacity eviction records a failed per-recipient receipt
|
|
462
462
|
instead of silently looking delivered. The send-time audience is fixed, unresolved targeted sends
|
|
463
|
-
fail closed, the core lane is machine-local,
|
|
464
|
-
into the brain.
|
|
463
|
+
fail closed, the core lane is machine-local, a note to every session expires after 24 hours and a
|
|
464
|
+
directed note after 7 days, and notes are never written into the brain.
|
|
465
465
|
|
|
466
466
|
Durable handoffs go in the brain itself — decisions, findings, open questions and skills captured
|
|
467
467
|
as cards, each stamped with the agent that wrote it.
|
|
@@ -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 |
|
|
@@ -730,19 +750,59 @@ replaceable worker runs the brain core. A staged update is hash-verified, initia
|
|
|
730
750
|
checked for backward-compatible tool schemas, and handed the current `brain_sync` task scope before
|
|
731
751
|
the supervisor switches between requests. Added tools use the standard
|
|
732
752
|
`notifications/tools/list_changed` signal. A failed or breaking candidate is rejected while the old
|
|
733
|
-
worker keeps serving. A
|
|
734
|
-
|
|
753
|
+
worker keeps serving. A connection idle for 10 minutes releases its worker and keeps its presence;
|
|
754
|
+
it stays asleep until its host sends a request, and the new worker it then starts passes the same
|
|
755
|
+
checks. If they reject it, the connection resumes the version it last ran, or answers with a
|
|
756
|
+
retryable `/mcp reconnect` error rather than restarting in a loop. A blocked result claim is kept
|
|
757
|
+
in a durable per-project/session marker, so a worker replacement cannot turn a failed evidence
|
|
758
|
+
check into a result-less completion.
|
|
735
759
|
|
|
736
760
|
Compatible engine updates therefore activate behind the same live connection — no reconnect, no
|
|
737
761
|
host restart. Three cases still require a deliberate reconnect or manual install: the one-time
|
|
738
|
-
legacy→supervisor migration, a supervisor-code change, and a major or tool-removing release.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
762
|
+
legacy→supervisor migration, a supervisor-code change, and a major or tool-removing release. Fixes
|
|
763
|
+
to the supervisor itself reach a connection only after one `/mcp` reconnect or a host restart;
|
|
764
|
+
worker and doctor changes hot-swap. `brain_doctor` reports the live supervisors (including how many
|
|
765
|
+
still run older supervisor code) and the automatic-update schedule: the last result, which install
|
|
766
|
+
it describes, and when the next check runs. The MCP tool also returns these as structured data.
|
|
767
|
+
|
|
768
|
+
The updater checks npm **once per machine every 6 hours**, however many sessions are open. It
|
|
769
|
+
checks once more, never sooner than 5 minutes after the last attempt, when another install upgrades
|
|
770
|
+
this runtime or moves it from a developer deploy to a released install. Examples are a manual
|
|
771
|
+
`npx klypix-mcp install` of a newer release, or a release replacing a developer deploy. A failed
|
|
772
|
+
check retries after 15 minutes, then 1 hour, then 4 hours, then returns to the 6-hour cadence. Each
|
|
773
|
+
attempt is recorded as failed before it touches the network, so a check that dies part-way backs
|
|
774
|
+
off instead of retrying in a loop. Checks run from the open KLYPIX sessions: the supervisor and
|
|
775
|
+
worker look every 10 minutes, and a new session looks 2 seconds after it starts.
|
|
776
|
+
|
|
777
|
+
The updater installs an exact stable release of the **same major version** in `--runtime-only`
|
|
778
|
+
mode, preserving host settings and project files; a new major always needs a manual install. It
|
|
779
|
+
never downgrades. A deliberate downgrade (`npx klypix-mcp@<older> install --force`) onto a release
|
|
780
|
+
newer than 1.89.0 is **held**: the updater does not re-install the version it was rolled back from,
|
|
781
|
+
only a newer release. To take the held version back, run `npx -y klypix-mcp@latest install` (or set
|
|
782
|
+
`KLYPIX_AUTO_UPDATE_FORCE=1` where `KLYPIX_AUTO_UPDATE` is set, below, for one check, then remove
|
|
783
|
+
it). A rollback onto 1.89.0 or earlier also rolls the updater back, and those releases have no
|
|
784
|
+
hold: they re-install the newest same-major release within 24 hours of their last check. To stay
|
|
785
|
+
on such a release, set `KLYPIX_AUTO_UPDATE=0` in every place listed below for as long as you stay;
|
|
786
|
+
`brain_doctor` warns when a downgrade is not held. The updater never fetches anything for a
|
|
787
|
+
developer-owned install and never installs over it. The check is detached and fail-open, and
|
|
788
|
+
concurrent sessions collapse behind one lock.
|
|
789
|
+
|
|
790
|
+
`KLYPIX_AUTO_UPDATE=0` opts out. Every process reads its own environment, so set it in each host's
|
|
791
|
+
launch environment: the `env` of each KLYPIX MCP server entry, and the environment Claude Code runs
|
|
792
|
+
its hooks in. It takes effect at that host's next supervisor start or `/mcp` reconnect.
|
|
793
|
+
|
|
794
|
+
There is one second, smaller probe. In a brain project (a directory with `./brain.klypix`), the
|
|
795
|
+
Claude Code Stop hook refreshes a local npm-version cache, which the next SessionStart reads to say
|
|
796
|
+
whether an update is available. That notice also says what the updater will do with the update,
|
|
797
|
+
and when. The probe:
|
|
798
|
+
|
|
799
|
+
- runs at most once a day, developer-owned installs included;
|
|
800
|
+
- makes the same kind of anonymous request the updater makes, a GET of
|
|
801
|
+
`https://registry.npmjs.org/klypix-mcp/latest` that carries no user or machine identifier;
|
|
802
|
+
- is skipped while the updater fetched npm's latest version less than a day ago;
|
|
803
|
+
- is off with the same `KLYPIX_AUTO_UPDATE=0`, read from the environment Claude Code runs its hooks
|
|
804
|
+
in;
|
|
805
|
+
- never installs anything.
|
|
746
806
|
|
|
747
807
|
When the optional semantic runtime is already enabled, an update also schedules one detached,
|
|
748
808
|
single-writer cache migration across registered brains. That removes the multi-minute first-query
|
|
@@ -754,9 +814,13 @@ keep lazy first-use indexing instead.
|
|
|
754
814
|
|
|
755
815
|
- **Apache-2.0, source public** at [github.com/dahshanlabs/klypix-mcp](https://github.com/dahshanlabs/klypix-mcp).
|
|
756
816
|
- **The brain engine makes no network calls and sends no telemetry.** All engine intelligence is
|
|
757
|
-
deterministic and local; the only LLM anywhere is *your* agent. The
|
|
758
|
-
is the
|
|
759
|
-
|
|
817
|
+
deterministic and local; the only LLM anywhere is *your* agent. The exceptions in this package
|
|
818
|
+
are the two update probes described above. One is the updater's npm version check, every 6
|
|
819
|
+
hours, plus one re-check after another install upgrades the runtime, and retries after a failed
|
|
820
|
+
check (15 minutes, 1 hour, 4 hours); when it finds a newer same-major release, it also runs that
|
|
821
|
+
release's npm install. The other is the Claude Code Stop hook's version probe, at most once a day
|
|
822
|
+
in brain projects. Both are the same kind of anonymous GET of the package's `latest` version, and
|
|
823
|
+
`KLYPIX_AUTO_UPDATE=0` turns both off.
|
|
760
824
|
- **The optional semantic model runs on device.** Enabling it (or upgrading its model) can fetch
|
|
761
825
|
model weights from Hugging Face; retrieval inference and brain data stay local.
|
|
762
826
|
- **Coordination state is local files.** The brain is a file in your repo; the presence lane is a
|
|
@@ -849,8 +913,8 @@ independently validated.
|
|
|
849
913
|
fewer than four content words: junk injection 93% → 35%, mean cards injected on junk 3.6 → 1.5,
|
|
850
914
|
at a one-question cost on the 35 real prompts (inside noise). The prompts stay private; the
|
|
851
915
|
sweep tables are in the source next to the bars they chose.
|
|
852
|
-
- **What we do not publish.** No download count: this package's own
|
|
853
|
-
|
|
916
|
+
- **What we do not publish.** No download count: this package's own auto-updater generates most
|
|
917
|
+
of it, so it is not a user count. No adoption, team or customer figures. No brief-token
|
|
854
918
|
figure — the last one was measured at ~600 cards and is stale at 2,479.
|
|
855
919
|
- **The eval harness is not in this repo.** It lives in the private KLYPIX desktop repository. The
|
|
856
920
|
numbers above are ours to defend, not yours to reproduce from here — treat them accordingly.
|
|
@@ -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-doctor.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// klypix-doctor — `npx klypix-mcp doctor`. The brain's self-check: is THIS machine's
|
|
3
3
|
// brain current, are the Claude capture and Codex presence adapters wired, what verbs
|
|
4
4
|
// does it expose, which lifecycle sessions are live, and is the harness projection in
|
|
5
|
-
// sync? ONE fact, ONE reconcile block. Read-only (never writes). Exits 0 = ALIGNED
|
|
6
|
-
// 1 = DRIFTED — so it
|
|
5
|
+
// sync? ONE fact, ONE reconcile block. Read-only (never writes). Exits 0 = ALIGNED
|
|
6
|
+
// or PARTIAL (readiness warnings, no drift), 1 = DRIFTED — so it
|
|
7
7
|
// doubles as a pre-commit / CI readiness gate.
|
|
8
8
|
//
|
|
9
9
|
// npx klypix-mcp doctor # this project + this machine's brain
|
|
@@ -63,9 +63,14 @@ try {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const drifted = report.verdict === 'DRIFTED' || extraDrift > 0;
|
|
66
|
+
// PARTIAL used to end with "✓ aligned." under a head that said PARTIAL
|
|
67
|
+
// (2026-10-03) — the closing line is what people read. Exit code unchanged.
|
|
68
|
+
const warnings = Array.isArray(report.readinessWarnings) ? report.readinessWarnings.length : 0;
|
|
66
69
|
console.log(drifted ? (color ? '\x1b[33m' : '') + `\n✗ drift found — see reconcile above.` + (color ? '\x1b[0m' : '')
|
|
67
70
|
: report.verdict === 'NOT-INSTALLED' ? '\n• brain not installed on this machine.'
|
|
68
|
-
:
|
|
71
|
+
: report.verdict === 'PARTIAL'
|
|
72
|
+
? (color ? '\x1b[33m' : '') + `\n• partial — ${warnings} readiness warning${warnings === 1 ? '' : 's'} above (no drift)` + (color ? '\x1b[0m' : '')
|
|
73
|
+
: (color ? '\x1b[32m' : '') + '\n✓ aligned.' + (color ? '\x1b[0m' : ''));
|
|
69
74
|
process.exit(drifted ? 1 : 0);
|
|
70
75
|
} catch (e) {
|
|
71
76
|
console.error(`✗ doctor failed: ${e?.message || e}`);
|
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 [
|