klypix-mcp 1.43.1 → 1.45.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 CHANGED
@@ -5,21 +5,35 @@
5
5
  your.klypix`), and parseable with the Apache-2.0 library in this package. You own it;
6
6
  any agent or app can read and write it.
7
7
 
8
+ Two working examples ship in this package under [`examples/`](examples/). Both are
9
+ text-and-arrows only (14 cards, no `assets/` entry), so they demonstrate the card /
10
+ container / connection model, not the embedded-binary half described below.
11
+
8
12
  ## Container layout
9
13
 
10
14
  ```
11
- your.klypix (a ZIP archive)
12
- ├── manifest.json metadata + stats
13
- ├── canvas.json spatial layout: order, positions, connections, lines, strokes, settings
15
+ your.klypix (a ZIP archive)
16
+ ├── manifest.json metadata + stats; read FIRST
17
+ ├── canvas.json spatial layout: order, positions, connections, lines, strokes, view, settings
14
18
  ├── items/
15
- │ └── <2-hex>/<id>.json one file per item (content only; position lives in canvas.json)
16
- └── assets/
17
- └── <assetId> embedded binaries (images, PDFs, audio, video, files)
19
+ │ └── <shard>/<id>.json one file per item (content only; geometry lives in canvas.json)
20
+ └── assets/ embedded binaries — any non-directory entry here is an asset
21
+ ├── images/<shard>/<sha256>.<ext>
22
+ ├── files/<shard>/<sha256>.bin
23
+ └── thumbs/<itemId>.png
18
24
  ```
19
25
 
20
- Item files are **sharded** by the first 2 hex chars of the id's random part
21
- (e.g. `items/a3/txt_a3f9…json`) so a canvas with thousands of items stays fast
22
- to read partially.
26
+ `<shard>` is the **first two characters of the id (or sha) after its namespace
27
+ prefix is stripped**, lowercased, left-padded with `_` if shorter — e.g.
28
+ `txt_hto5hb3r_0_0` → `items/ht/txt_hto5hb3r_0_0.json`, and sha `a3f9…` →
29
+ `assets/images/a3/a3f9….png`. Ids are base36-ish, not strictly hex, so treat the
30
+ shard as "two characters", not "two hex digits". Sharding bounds files-per-directory
31
+ and lets a canvas with thousands of items be read partially.
32
+
33
+ Readers should treat the `assets/` sub-layout as **convention, not contract**: this
34
+ package's parser counts and reads *any* non-directory entry under `assets/`, so a
35
+ writer that flattens to `assets/<assetId>` still parses. Only the KLYPIX app's own
36
+ writer guarantees the content-addressed `images/ files/ thumbs/` split.
23
37
 
24
38
  ## `manifest.json`
25
39
 
@@ -28,13 +42,30 @@ to read partially.
28
42
  "format": "klypix",
29
43
  "version": 4,
30
44
  "schemaVersion": 4,
45
+ "kind": "brain",
31
46
  "createdAt": "ISO-8601",
32
47
  "updatedAt": "ISO-8601",
33
48
  "title": "My board",
34
- "stats": { "itemCount": 12, "assetCount": 3, "totalBytes": 0 }
49
+ "stats": { "itemCount": 12, "assetCount": 3, "totalBytes": 0 },
50
+ "sync": { "enabled": false, "lastSyncRev": null, "lastSyncAt": null, "deviceId": "dev_…" }
35
51
  }
36
52
  ```
37
53
 
54
+ - `version` — on-disk **layout** version (the container shape).
55
+ - `schemaVersion` — **document** version (item/connection shapes).
56
+ - **`kind: "brain"`** — optional and additive; present only on a **project brain**.
57
+ A brain is the co-owned agent/human memory file, and it gets stricter write
58
+ semantics: union-merge-on-save under a capture lock, tombstone-only deletes. It is
59
+ detected as `manifest.kind === "brain"` **OR** a `brain.*` basename — the filename
60
+ convention keeps working forever, and the flag makes those semantics survive a
61
+ rename. Plain canvases omit `kind` entirely, and older readers ignore it.
62
+ - `stats` is **informational only.** `totalBytes` is an uncompressed estimate (the
63
+ on-disk size depends on compression), and `itemCount` is derived from
64
+ `canvas.json.order`, which can disagree with the number of files actually under
65
+ `items/`. Never use `stats` as an integrity check.
66
+ - `sync` records whether the file is opted into the app's cloud sync; it carries no
67
+ content and no credentials.
68
+
38
69
  ## `canvas.json`
39
70
 
40
71
  ```json
@@ -46,21 +77,34 @@ to read partially.
46
77
  "<id>": { "x": 0, "y": 0, "w": 280, "h": 80, "zKey": "a001", "zIndex": 1, "parentId": null }
47
78
  },
48
79
  "connections": [ // arrows between items
49
- { "id": "con_…", "fromId": "<id>", "toId": "<id>", "relationship": "leads_to", "arrowHead": true }
80
+ { "id": "con_…", "fromId": "<id>", "toId": "<id>",
81
+ "relationship": "conflicts_with", "label": "corrects", // both optional
82
+ "arrowHead": true, "width": 2, "color": "#10b981", "style": "solid" }
50
83
  ],
51
84
  "lines": [], "strokes": [], // freehand drawing
52
- "settings": { "background": "#0a0a0f" }
85
+ "nextGroupNumber": 4, // container auto-naming counter
86
+ "settings": { "background": "#0a0a0f", "gridStyle": "dots", "gridColor": "…" } // all optional
53
87
  }
54
88
  ```
55
89
 
56
90
  Position/geometry is kept in `canvas.json.positions`, **not** in the item file —
57
91
  so moving an item never rewrites its content, and the spatial layout can be read
58
- without loading every item.
92
+ without loading every item. `zKey` is a fractional index and is the source of truth
93
+ for z-order; `zIndex` is a numeric cache kept in step with the `order[]` index.
94
+ `settings` is optional and captures the *sender's* visual context, so a recipient
95
+ does not see someone else's canvas in their own theme.
96
+
97
+ `order` and `positions` are the two indexes a reader walks. A tolerant reader skips
98
+ an `order` entry with no `positions` row and an entry with no file under `items/`
99
+ (this package's parser does both) — but note that a writer which then re-serializes
100
+ that state can make the omission permanent.
59
101
 
60
102
  ## Item files — `items/<shard>/<id>.json`
61
103
 
62
- Content only (no x/y/w/h). Common types: `text`, `box`, `image`, `file`,
63
- `code`, `video`, `audio`, `link`, `canvasLink`, `container`. Example text item:
104
+ Content only — no `id` (it is in the path) and no `x/y/w/h/zIndex/zKey/parentId`
105
+ (those are in `canvas.json.positions`). The `type` field is one of exactly eleven
106
+ strings: `text`, `box`, `image`, `file`, `container`, `approval`, `link`,
107
+ `canvas-link`, `video`, `audio`, `code`. Example text item:
64
108
 
65
109
  ```json
66
110
  {
@@ -70,12 +114,44 @@ Content only (no x/y/w/h). Common types: `text`, `box`, `image`, `file`,
70
114
  "color": "#e8e8ed",
71
115
  "border": true,
72
116
  "heading": false,
73
- "createdBy": "agent"
117
+ "createdBy": "agent",
118
+ "createdVia": "claude-code"
74
119
  }
75
120
  ```
76
121
 
77
- Items can reference an embedded binary in `assets/` (e.g. an `image`/`file`/
78
- `video`/`audio` item points at its asset id).
122
+ `createdBy` (`user` | `agent`) and the optional `createdVia` (which agent/channel
123
+ captured it) are the provenance bits the brain surfaces as badges and lenses.
124
+
125
+ ## Bytes: when they are embedded vs referenced
126
+
127
+ **Embedded** — the bytes live inside the ZIP under `assets/`, and the item JSON
128
+ references them by key/sha:
129
+
130
+ | Item type | What is embedded |
131
+ |---|---|
132
+ | `image` | the original image (plus an optional downscaled `thumbnailAssetId`). Non-destructive crop/rotate/annotation data lives in the item JSON; the original asset stays canonical. |
133
+ | `file` | the original file bytes. A **folder card** (`isFolder: true`) embeds the whole directory as one ZIP asset, with a `folderManifest` listing what's inside so the card renders without unzipping. |
134
+ | `video`, `audio` | the media bytes, streamed from the asset at render time rather than loaded whole. |
135
+
136
+ **Referenced, never embedded** — nothing is copied into the file:
137
+
138
+ | Item type | What it points at |
139
+ |---|---|
140
+ | `link` | a remote `url` (+ cached Open Graph title/description; the OG image is fetched live, not stored) |
141
+ | `canvas-link` | another `.klypix` file by **absolute path on disk** — so it breaks if you email the file alone |
142
+ | `text`, `code`, `box`, `container`, `approval` | content is inline in the item JSON; there is no asset at all |
143
+
144
+ `file` / `video` / `audio` items may also carry `originalPath` — a convenience
145
+ pointer for "open in the OS app". The bytes are still embedded; the path is not the
146
+ source of truth.
147
+
148
+ Legacy files may carry an `image` item's bytes as an inline base64 `src` data URL
149
+ instead of an asset. New writers use `assetId` and leave `src` empty.
150
+
151
+ **This package reads assets but does not create them.** `create_canvas`,
152
+ `add_to_canvas` and `buildKlypix` write `manifest.json`, `canvas.json` and item
153
+ files — cards and arrows. Embedding binaries is done by the KLYPIX app when you drop
154
+ a file onto a canvas.
79
155
 
80
156
  ## Connections, links, tags
81
157
 
@@ -85,10 +161,56 @@ Items can reference an embedded binary in `assets/` (e.g. an `image`/`file`/
85
161
  - **`[[wikilinks]]`** inside text content cross-link cards (and auto-draw edges).
86
162
  - **`#tags`** inside text content group cards.
87
163
 
164
+ ## Versioning — and a real limitation
165
+
166
+ `manifest.version` is the layout version and `manifest.schemaVersion` the document
167
+ version. Both are `4` today.
168
+
169
+ **There is no migration path registered for v4, and the v4 load path does not run
170
+ one.** Be precise about what that means, because it is a live limitation, not a
171
+ to-do that is quietly handled:
172
+
173
+ - The migration framework exists and works, but its table contains exactly two
174
+ steps, `1 → 2` and `2 → 3`. Only the legacy `.any` (v1–v3) read path walks it; the
175
+ v4 path never enters the migration runner.
176
+ - Both this package's parser and the KLYPIX app's v4 reader treat
177
+ `manifest.version >= 4` as "this is v4" and dispatch on nothing else — the app's
178
+ deserializer never reads `manifest.version`, `manifest.schemaVersion` or
179
+ `canvas.json.version` at all.
180
+ - So a hypothetical v5 or v6 file **opens as v4**: fields the reader does not know
181
+ are dropped, with no "this build is too old to open it" error anywhere. Saving it
182
+ from the app then rewrites `version: 4` — a silent downgrade. (This package's
183
+ `appendToKlypix` is non-destructive by construction: it mutates the parsed
184
+ manifest and `canvas.json` in place and leaves every existing item file byte-for-byte
185
+ untouched, so unknown fields survive a round trip through it.)
186
+
187
+ Consequences for anyone implementing against this format: do not assume a future
188
+ version will be rejected for you, and if you write `.klypix` files, do not bump
189
+ `manifest.version` past 4 expecting existing readers to refuse them safely. If you
190
+ extend the format, prefer **additive** optional fields (the way `manifest.kind` was
191
+ added) over a version bump.
192
+
88
193
  ## Legacy `.any` (v1–v3)
89
194
 
90
195
  Older files keep an inline `items` array at the root of `canvas.json` instead of
91
- the `items/` folder + `positions` map. The parser in this package handles both.
196
+ the `items/` folder + `positions` map. The parser in this package handles both, and
197
+ detects the older shape by the absence of `positions` even if the manifest is
198
+ missing. Legacy files are migrated `1 → 2 → 3` on open by the app.
199
+
200
+ ## Practical size limits
201
+
202
+ The format itself imposes no size limit — a `.klypix` is a ZIP. The limits below are
203
+ **guards in the KLYPIX app**, not properties of the format, and this package's
204
+ parser enforces none of them:
205
+
206
+ | Limit | Value | Where it applies |
207
+ |---|---|---|
208
+ | Per-file (leaf) on folder ingest | **200 MB** | a single file inside a dropped folder; anything larger is skipped and recorded in the card's `folderSkipped` list so you can see what was left out |
209
+ | Per folder card, total | **1 GB** | ingestion stops once a dropped folder would push the card past this |
210
+ | Per cloud share | **50 MB** | sharing a canvas through the app's cloud; a larger canvas is refused with a `too-large` result rather than truncated |
211
+
212
+ Nothing stops you writing a larger file locally with this library; expect the app's
213
+ share and folder-ingest paths to refuse it.
92
214
 
93
215
  ## Read / write it
94
216
 
@@ -102,4 +224,11 @@ const buf = await buildKlypix({ title: 'Plan', cards: [{ text: 'kickoff' }] });
102
224
  fs.writeFileSync('plan.klypix', buf);
103
225
  ```
104
226
 
227
+ Or from a shell, against one of the examples in the tarball:
228
+
229
+ ```bash
230
+ npm i klypix-mcp
231
+ npx klypix-read node_modules/klypix-mcp/examples/showcase-brain.klypix
232
+ ```
233
+
105
234
  That's the whole contract: **a ZIP you own, that any model can read and write.**
package/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ klypix-mcp
2
+ Copyright 2026 Abdullah Aldahshan — Dahshan Labs
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
5
+ Prior published versions (<= 1.28.0) were released under the MIT License and
6
+ remain available under those terms.
package/README.md CHANGED
@@ -85,16 +85,28 @@ It verifies 7 coordination behaviours — not the 18 tools, and not the retrieva
85
85
 
86
86
  ## Quick start
87
87
 
88
- **Claude Code + Codex — one machine-global command:**
88
+ **Claude Code + Codex:**
89
89
 
90
90
  ```bash
91
91
  npx klypix-mcp install
92
92
  ```
93
93
 
94
94
  This copies the engine and a local MCP runtime into `~/.claude/project-brain`, wires Claude Code's
95
- four lifecycle hooks, and wires Codex's project MCP connection. It is machine-global: every project
96
- on that machine with a `./brain.klypix` is covered. It does **not** set up Cursor, Cline, Windsurf,
97
- Copilot, Gemini CLI or Aider — those need `link`.
95
+ four lifecycle hooks, writes Codex's global `~/.codex/AGENTS.md` guidance block, and wires Codex's
96
+ MCP connection **for the project you run it in**.
97
+
98
+ Be precise about what "machine-global" covers:
99
+
100
+ - **Machine-global** — the engine + runtime in `~/.claude/project-brain`, the four Claude Code
101
+ hooks in `~/.claude/settings.json`, and the `~/.codex/AGENTS.md` guidance. Claude Code is
102
+ therefore covered in every project on that machine that has a `./brain.klypix`.
103
+ - **Per project** — Codex's MCP connection. `install` writes it into `<cwd>/.codex/config.toml`,
104
+ only when that directory has a `brain.klypix`, and it deliberately **removes** any *global*
105
+ `~/.codex/config.toml` KLYPIX entry (a global entry resolves its `--vault` from the wrong
106
+ directory and binds the wrong brain). Run `install` — or `link` — once inside each brain project
107
+ you want Codex wired to.
108
+
109
+ It does **not** set up Cursor, Cline, Windsurf, Copilot, Gemini CLI or Aider — those need `link`.
98
110
 
99
111
  Optional, opt-in, and approved inside Codex itself:
100
112
 
@@ -174,8 +186,9 @@ behaviour is unverified.
174
186
  | **Aider** | Rules file only (no MCP) | `link` | CLI path: `npx klypix-read` | CLI path: `npx klypix-append` | — |
175
187
  | **Claude Desktop** | One-time manual config edit | you | Model must call `brain_sync` | Model must call `brain_note` | For the MCP connection |
176
188
 
177
- `install` and `link` are different things and are not interchangeable: `install` is machine-global
178
- and only touches Claude Code and Codex; `link` is per project and is what wires everything else.
189
+ `install` and `link` are different things and are not interchangeable: `install` only touches Claude
190
+ Code and Codex, and is machine-global for everything except Codex's MCP connection, which it writes
191
+ per project (see *Quick start*); `link` is per project and is what wires everything else.
179
192
 
180
193
  **Claude Desktop** — add this to `claude_desktop_config.json` by hand; nothing writes that file
181
194
  for you:
@@ -329,10 +342,12 @@ Exactly 18, machine-verifiable with `npx klypix-mcp doctor`.
329
342
 
330
343
  ## One file you can hold
331
344
 
332
- The whole brain — layout, cards, arrows, and the actual bytes (images, PDFs, audio, code) — is a
345
+ The whole brain — layout, cards, arrows, and the actual bytes (images, PDFs, audio, video) — is a
333
346
  single `.klypix` file: a plain ZIP with `manifest.json`, `canvas.json`, one JSON file per card, and
334
347
  an `assets/` folder. Email it. Git it. Hand it to an agent. A folder of markdown points at its
335
- attachments; this file carries them.
348
+ attachments; this file carries them. (Binaries are embedded by the **KLYPIX app** when you drop a
349
+ file onto a canvas; this package's `create_canvas` / `add_to_canvas` / `buildKlypix` write cards and
350
+ arrows, not assets — they read assets fine, they just don't create them.)
336
351
 
337
352
  The parser is this package, Apache-2.0, so any tool or agent can read and write the format. Full
338
353
  spec: [FORMAT.md](FORMAT.md).
@@ -341,14 +356,24 @@ Markdown export, JSON Canvas 1.0 export and direct opening of Obsidian `.canvas`
341
356
  of the **KLYPIX desktop app**, not of this package — there is no export command among this
342
357
  package's binaries.
343
358
 
344
- **"Project" means any project.** Two showcase brains ship in the GitHub repo under
345
- [`examples/`](examples/), identical in engine, different in life:
359
+ **"Project" means any project.** Two showcase brains ship in the npm package *and* the GitHub repo
360
+ under [`examples/`](examples/), identical in engine, different in life:
346
361
  [`showcase-brain.klypix`](examples/showcase-brain.klypix) is *Aurora*, a fictional weather app
347
362
  mid-build (radar tiles, API caps, a correction with its receipt), and
348
363
  [`showcase-wedding.klypix`](examples/showcase-wedding.klypix) is *Our Wedding* (venue, vendors,
349
364
  guest list, the same correction machinery pointed at a caterer). Same 📌 Focus, same arrows, same
350
365
  brief. If it has decisions worth keeping, it gets a brain.
351
366
 
367
+ They ship inside the tarball, so you can read one straight out of `node_modules`:
368
+
369
+ ```bash
370
+ npm i klypix-mcp
371
+ npx klypix-read node_modules/klypix-mcp/examples/showcase-brain.klypix
372
+ ```
373
+
374
+ Both are text-and-arrows only — 14 cards, 4 arrows, no `assets/` entry — so they demonstrate the
375
+ card / container / connection model, not the embedded-binaries half of the format.
376
+
352
377
  ## Use it as a library
353
378
 
354
379
  ```js
@@ -407,9 +432,11 @@ entirely.
407
432
  - **Coordination state is local files.** The brain is a file in your repo; the presence lane is a
408
433
  file under your home directory. Nothing is uploaded.
409
434
  - **`install` writes to your home directory:** `~/.claude/project-brain` (engine + runtime),
410
- `~/.claude/settings.json` (four hooks — written even if Claude Code is not installed) and Codex
411
- config. **`link` writes 14 files inside the project** you run it in; `link --check` audits them
412
- without writing.
435
+ `~/.claude/settings.json` (four hooks — written even if Claude Code is not installed),
436
+ `~/.codex/AGENTS.md` (guidance block), and with `--codex-hooks`, `~/.codex/hooks.json`. It also
437
+ writes `<cwd>/.codex/config.toml` **inside the project** you run it in, and removes any KLYPIX
438
+ entry from the global `~/.codex/config.toml`. **`link` writes 14 files inside the project** you
439
+ run it in; `link --check` audits them without writing.
413
440
  - **Codex hooks require Codex's own trust approval** and are opt-in via `--codex-hooks`.
414
441
 
415
442
  ## Current limitations
@@ -498,5 +525,15 @@ and write.
498
525
 
499
526
  ---
500
527
 
501
- Apache-2.0 © [Dahshan Labs](https://klypix.com). The KLYPIX desktop app is a separate, proprietary
502
- product — the format and this server are fully open and work without it.
528
+ ## Licence
529
+
530
+ This package — the MCP server, the agent hooks and the `.klypix` format parser — is
531
+ **Apache-2.0** ([`LICENSE`](LICENSE), attribution in [`NOTICE`](NOTICE)). Versions up to and
532
+ including **1.28.0** were published under MIT and remain available under those terms; **1.29.0** was
533
+ the first Apache-2.0 release.
534
+
535
+ The KLYPIX desktop app and the klypix.com web app are **separate, proprietary products** — their
536
+ source is not public, and their terms do not restrict anything Apache-2.0 grants you here. This
537
+ package works with no app installed.
538
+
539
+ Apache-2.0 © [Dahshan Labs](https://klypix.com).
@@ -17,6 +17,10 @@ import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/type
17
17
  import { buildKlypixMap } from '../src/klypix-core.mjs';
18
18
 
19
19
  const here = path.dirname(fileURLToPath(import.meta.url));
20
+ // ARGV: flag lookup is position-independent, so this file is already correct for
21
+ // both `klypix-conformance --json` and `klypix-mcp conformance --json` (the
22
+ // dispatcher splices its verb out — see bin/klypix-worker.mjs runVerb). Kept as
23
+ // `includes` deliberately; test/cli-args.mjs asserts the two forms stay identical.
20
24
  const jsonMode = process.argv.includes('--json');
21
25
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
26
  const PKG_VERSION = (() => {
@@ -13,12 +13,21 @@
13
13
  //
14
14
  // Synchronous-ish top-level by design: the dispatcher does `await import(this);
15
15
  // process.exit(...)`, so all work completes during module evaluation.
16
+ import fs from 'fs';
16
17
  import path from 'path';
17
18
  import { execSync } from 'child_process';
18
19
  import { inspect, render, inspectAll } from '../src/brain-doctor.mjs';
19
20
 
21
+ // ARGV: same dual shape as klypix-link — this file is both `klypix-doctor` and the
22
+ // target of `klypix-mcp doctor` (the dispatcher splices its verb out first, see
23
+ // bin/klypix-worker.mjs runVerb). slice(2) is the one correct shape; the strip is
24
+ // belt-and-braces for a dispatcher that did not splice, and refuses to eat a
25
+ // directory actually named ./doctor.
26
+ const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
27
+
20
28
  try {
21
- const argv = process.argv.slice(2);
29
+ const raw = process.argv.slice(2);
30
+ const argv = (raw[0] === 'doctor' && !isDir(path.resolve(raw[0]))) ? raw.slice(1) : raw;
22
31
  const has = (f) => argv.includes(f);
23
32
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
24
33
  const color = !has('--no-color') && process.stdout.isTTY !== false;
@@ -34,6 +34,11 @@ const PKG_ROOT = path.resolve(__dirname, '..');
34
34
  const SRC = path.join(PKG_ROOT, 'src');
35
35
  const BIN = path.join(PKG_ROOT, 'bin');
36
36
  const VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version || ''; } catch { return ''; } })();
37
+ // ARGV: flag lookup is position-independent, so this file is correct for both
38
+ // `klypix-install --force` and `klypix-mcp install --force` (the dispatcher splices
39
+ // its verb out before importing — see bin/klypix-worker.mjs runVerb). It takes no
40
+ // positional argument; if one is ever added, read process.argv.slice(2) so both
41
+ // invocation shapes stay identical (locked by test/cli-args.mjs).
37
42
  const FORCE = process.argv.includes('--force');
38
43
  const CODEX_HOOKS = process.argv.includes('--codex-hooks');
39
44
  const RUNTIME_ONLY = process.argv.includes('--runtime-only');
@@ -16,11 +16,24 @@
16
16
  //
17
17
  // Synchronous top-level by design: the dispatcher does `await import(this); process.exit(0)`,
18
18
  // so all work (and its logs) must complete during module evaluation, before exit.
19
+ import fs from 'fs';
19
20
  import path from 'path';
20
21
  import { compactAgentsBrief, linkProject } from '../src/agent-rules.mjs';
21
22
 
23
+ // ARGV: this file is BOTH `klypix-link` (its own published bin) and the target of
24
+ // `klypix-mcp link` (the dispatcher splices its verb out of process.argv before
25
+ // importing us — see bin/klypix-worker.mjs runVerb). slice(2) is therefore the ONE
26
+ // correct shape for both. It used to be slice(3), which is right only via the
27
+ // dispatcher: as a standalone bin `klypix-link --check` silently dropped --check
28
+ // and WROTE all 14 managed files with exit 0, and `klypix-link <dir>` wrote into
29
+ // the cwd instead of <dir>. The strip below is belt-and-braces for a dispatcher
30
+ // that did not splice, and it refuses to eat a project folder actually named
31
+ // ./link.
32
+ const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
33
+
22
34
  try {
23
- const args = process.argv.slice(3);
35
+ const raw = process.argv.slice(2);
36
+ const args = (raw[0] === 'link' && !isDir(path.resolve(raw[0]))) ? raw.slice(1) : raw;
24
37
  const check = args.includes('--check');
25
38
  const dirArg = args.find(a => !a.startsWith('-'));
26
39
  const projectDir = path.resolve(dirArg || process.cwd());
@@ -56,6 +69,20 @@ try {
56
69
  }
57
70
 
58
71
  const changed = [...rules, ...mcp, compactBrief].filter(r => r.action && !['unchanged', 'skipped'].includes(r.action)).length;
72
+ // A SKIPPED target is not wired (invalid MCP JSON left untouched, or a Codex
73
+ // TOML write that failed). Write mode used to filter those out of the count,
74
+ // print the blanket "every agent … now reads + captures" line anyway, and exit
75
+ // 0 — while `--check` on the same project correctly reported drift and exited 1.
76
+ // Report the truth and fail, so the two modes agree.
77
+ const skipped = [...rules, ...mcp].filter(r => r.action === 'skipped');
78
+ const managed = rules.length + mcp.length;
79
+ if (skipped.length) {
80
+ console.log(`\n✗ ${skipped.length} target(s) could not be wired:`);
81
+ for (const r of skipped) console.log(` ⚠ ${r.tool.padEnd(26)} ${r.file}${r.why ? ' — ' + r.why : ''}`);
82
+ console.log(`\n wired ${managed - skipped.length} of ${managed} managed file(s) (${changed} written/updated this run).`);
83
+ console.log(' Fix the file(s) above (or move them aside) and re-run `npx klypix-mcp link`.');
84
+ process.exit(1);
85
+ }
59
86
  console.log(`\n✓ ${changed} file(s) written/updated — every agent opened in this project now reads + captures ./brain.klypix.`);
60
87
  console.log(' Codex, Cline, Gemini/Antigravity, Cursor, and VS Code get project MCP configs; Windsurf uses its global MCP plus project rules.');
61
88
  console.log(' Verify anytime with `npx klypix-mcp link --check` (or `npx klypix-mcp doctor`).');
@@ -20,7 +20,38 @@ const PKG_VERSION = (() => {
20
20
  })();
21
21
 
22
22
  const DIRECT = new Set(['install', 'link', 'doctor', 'conformance', 'garden-code', 'init']);
23
- if (DIRECT.has(process.argv[2])) {
23
+
24
+ const USAGE = [
25
+ `klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
26
+ '',
27
+ 'Verbs:',
28
+ ' install [--force] [--codex-hooks] install/update this machine\'s brain engine + Claude Code hooks',
29
+ ' link [dir] [--check] project this project\'s 14 managed agent config files (--check audits, writes nothing, exits 1 on drift)',
30
+ ' doctor [--npm] [--all] [--json] read-only self-check; exits 1 on drift',
31
+ ' conformance [--json] launch two real MCP clients against this build',
32
+ ' init seed a starter ./brain.klypix + print an MCP config',
33
+ ' garden-code [brain] print the human approval code for brain_garden',
34
+ '',
35
+ 'With no verb (or any --flag, e.g. --vault <dir>) it runs as an MCP stdio server.',
36
+ 'There is no uninstall command — removal is manual (see README).',
37
+ ].join('\n');
38
+
39
+ const verb = process.argv[2];
40
+ if (verb === '--help' || verb === '-h' || verb === 'help') {
41
+ console.log(USAGE);
42
+ process.exit(0);
43
+ }
44
+ // A bare unknown WORD used to fall through to the stdio server, which then sat
45
+ // waiting on a stdin that no host was driving — so `npx klypix-mcp uninstall`
46
+ // (or any typo) printed nothing and exited 0, indistinguishable from success.
47
+ // Only non-dash tokens are rejected: a real host launch is `--vault <dir>`,
48
+ // and the local-bundle launch form puts '--vault' at argv[2] too.
49
+ if (verb && !verb.startsWith('-') && !DIRECT.has(verb)) {
50
+ console.error(`klypix-mcp: unknown command "${verb}".\n\n${USAGE}`);
51
+ process.exit(2);
52
+ }
53
+
54
+ if (DIRECT.has(verb)) {
24
55
  await import('./klypix-worker.mjs');
25
56
  } else {
26
57
  const { runMcpSupervisor } = await import('../src/mcp-supervisor.mjs');