klypix-mcp 1.45.1 → 1.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.45.1",
3
+ "version": "1.46.0",
4
4
  "description": "Shared project brain and MCP coordination server for multi-agent coding.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,7 +65,7 @@
65
65
  "node": ">=18"
66
66
  },
67
67
  "scripts": {
68
- "test": "node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/presence-visibility.mjs && node test/cli-args.mjs && node test/uninstall.mjs"
68
+ "test": "node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/presence-visibility.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/uninstall.mjs"
69
69
  },
70
70
  "dependencies": {
71
71
  "@modelcontextprotocol/ext-apps": "^1.7.4",
@@ -9,8 +9,36 @@
9
9
  import JSZip from 'jszip';
10
10
  import path from 'path';
11
11
  import fs from 'fs';
12
+ import os from 'os';
13
+ import { execFileSync } from 'child_process';
12
14
  import { generateKeyBetween } from 'fractional-indexing';
13
15
 
16
+ // ── Card author identity (team attribution, 2026-08-01) ─────────────────────
17
+ // `createdBy: 'agent'` says WHAT wrote a card; on a team brain the question is
18
+ // WHOSE agent. Identity rides the same source as the dev's commits — git
19
+ // `user.name` in the project — so brain attribution matches git attribution
20
+ // with zero configuration; `KLYPIX_AUTHOR` overrides, OS account name is the
21
+ // fallback, and on total failure the field is simply absent (additive — older
22
+ // readers and older cards are untouched; merge preserves item bytes verbatim,
23
+ // so authorship survives every sync route).
24
+ let cachedAuthor;
25
+ export function resolveAuthor() {
26
+ if (cachedAuthor !== undefined) return cachedAuthor;
27
+ const env = String(process.env.KLYPIX_AUTHOR || '').trim();
28
+ if (env) return (cachedAuthor = env.slice(0, 80));
29
+ try {
30
+ const name = execFileSync('git', ['config', 'user.name'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 })
31
+ .toString().trim();
32
+ if (name) return (cachedAuthor = name.slice(0, 80));
33
+ } catch { /* not a repo / git absent — fall through */ }
34
+ try { cachedAuthor = String(os.userInfo().username || '').slice(0, 80) || null; }
35
+ catch { cachedAuthor = null; }
36
+ return cachedAuthor;
37
+ }
38
+ // Test seam: clears the per-process cache so env overrides can be exercised.
39
+ export function __resetAuthorCache() { cachedAuthor = undefined; }
40
+ const authorField = () => { const a = resolveAuthor(); return a ? { author: a } : {}; };
41
+
14
42
  // Valid fractional-indexing z-keys. Hand-rolled keys (e.g. 'a0000' / 'z00013')
15
43
  // are REJECTED by the fractional-indexing lib the KLYPIX app uses and crash it
16
44
  // the moment you edit such a canvas — so the writer MUST emit lib-valid keys.
@@ -91,6 +119,20 @@ export async function parseKlypix(buffer) {
91
119
  if (!canvasRaw) throw new Error('Not a valid .klypix/.any — no canvas.json inside.');
92
120
 
93
121
  const manifest = manifestRaw ? JSON.parse(manifestRaw) : null;
122
+ // Forward-compat guard: this engine reads format v4. A file stamped by a
123
+ // FUTURE format must be refused loudly, never parsed blindly — with brains
124
+ // syncing between machines (git / Brain Sync), mixed versions are a
125
+ // guaranteed state, and a blind parse here feeds every downstream writer
126
+ // (merge, arrange, capture) a structure it does not understand. The merge
127
+ // driver inherits this automatically: the throw exits it non-zero, which
128
+ // degrades to a normal manual git conflict.
129
+ const KLYPIX_FORMAT_CEILING = 4;
130
+ if (manifest && manifest.format === 'klypix' && Number(manifest.version) > KLYPIX_FORMAT_CEILING) {
131
+ throw new Error(
132
+ `This .klypix was saved by a newer format (v${manifest.version}); this engine reads up to v${KLYPIX_FORMAT_CEILING}. ` +
133
+ 'Update KLYPIX / klypix-mcp instead of parsing it — a blind read could damage it.'
134
+ );
135
+ }
94
136
  const canvas = JSON.parse(canvasRaw);
95
137
  // v4 manifests are {format:"klypix", version:4}; positions presence is the
96
138
  // robust fallback (legacy .any keeps an inline items array, no positions).
@@ -272,7 +314,7 @@ export async function buildKlypix(spec) {
272
314
  const itemJson = (card, w) => {
273
315
  if (card.type === 'text') {
274
316
  return {
275
- type: 'text', locked: false, createdAt: now, createdBy: 'agent',
317
+ type: 'text', locked: false, createdAt: now, createdBy: 'agent', ...authorField(),
276
318
  content: String(card.text ?? ''), fontSize: FONT,
277
319
  // PLAIN text (no border) renders at max-content width unless
278
320
  // authoredWidth pins the wrap — without it a long single line
@@ -284,7 +326,7 @@ export async function buildKlypix(spec) {
284
326
  textDecoration: 'none', textAlign: 'left', verticalAlign: 'top',
285
327
  };
286
328
  }
287
- return { type: card.type, locked: false, createdAt: now, createdBy: 'agent', ...(card._raw || {}) };
329
+ return { type: card.type, locked: false, createdAt: now, createdBy: 'agent', ...authorField(), ...(card._raw || {}) };
288
330
  };
289
331
 
290
332
  const zip = new JSZip();
@@ -369,7 +411,7 @@ export async function appendToKlypix(buffer, addition) {
369
411
  const nextZKey = makeZKeyGen(existingTop);
370
412
  for (const a of added) {
371
413
  zip.file(`items/${shard(a.id)}/${a.id}.json`, JSON.stringify({
372
- type: 'text', locked: false, createdAt: now, createdBy: 'agent',
414
+ type: 'text', locked: false, createdAt: now, createdBy: 'agent', ...authorField(),
373
415
  ...(a.card.createdVia ? { createdVia: String(a.card.createdVia) } : {}),
374
416
  content: String(a.card.text), fontSize: FONT,
375
417
  color: a.card.color || '#1a1a1f', border: !!a.card.border, borderColor: '#1e1e2e',
@@ -542,8 +584,10 @@ export async function appendIntoContainers(buffer, addition) {
542
584
  const id = `txt_${rand()}`;
543
585
  zip.file(`items/${shard(id)}/${id}.json`, JSON.stringify({
544
586
  type: 'text', locked: false, createdAt: now, createdBy: 'agent',
545
- // Provenance: WHICH agent remembered this (claude-code / cursor /
546
- // cline / …) — additive field, ignored by older readers.
587
+ // Provenance: WHOSE agent (git user.name — matches commit identity)…
588
+ ...authorField(),
589
+ // …and WHICH agent remembered this (claude-code / cursor /
590
+ // cline / …) — both additive fields, ignored by older readers.
547
591
  ...(card.createdVia ? { createdVia: String(card.createdVia) } : {}),
548
592
  // Evidence anchors (file:line / PR#) — additive, ignored by older readers.
549
593
  ...(Array.isArray(card.evidence) && card.evidence.length ? { evidence: card.evidence } : {}),
@@ -3837,7 +3881,7 @@ export async function applyGarden(buffer, { syntheses = [] } = {}) {
3837
3881
  // `sources` = machine lineage (which originals fed this synthesis, with
3838
3882
  // their birth dates) — as_of and future provenance passes read the
3839
3883
  // field, never the prose.
3840
- zip.file(`items/${shard(sid)}/${sid}.json`, JSON.stringify({ type: 'text', locked: false, createdAt: now, createdBy: 'agent', createdVia: 'gardener', content, fontSize: 12, color: '#e8e8ed', border: true, borderColor: 'rgba(59,130,246,0.6)', heading: false, sources: area.candidates.map(c => ({ id: c.id, createdAt: c.createdAt || 0 })) }));
3884
+ zip.file(`items/${shard(sid)}/${sid}.json`, JSON.stringify({ type: 'text', locked: false, createdAt: now, createdBy: 'agent', ...authorField(), createdVia: 'gardener', content, fontSize: 12, color: '#e8e8ed', border: true, borderColor: 'rgba(59,130,246,0.6)', heading: false, sources: area.candidates.map(c => ({ id: c.id, createdAt: c.createdAt || 0 })) }));
3841
3885
  canvas.positions[sid] = { x: ctnPos.x + 20, y: ctnPos.y + (ctnPos.h || 0) + 10, w: 300, h: measureCardH(content), zKey: nextZKey(), zIndex: canvas.order.length, parentId: area.containerId };
3842
3886
  canvas.order.push(sid);
3843
3887
  newCards.push(sid);
@@ -4617,7 +4661,7 @@ export async function buildKlypixMap(spec) {
4617
4661
  const id = `txt_${rand()}_${ai}_${ci}`;
4618
4662
  const h = measured[ci];
4619
4663
  items[id] = {
4620
- type: 'text', locked: false, createdAt: now, createdBy: 'agent',
4664
+ type: 'text', locked: false, createdAt: now, createdBy: 'agent', ...authorField(),
4621
4665
  content: String(c.text), fontSize: FONT,
4622
4666
  color: c.color || '#e8e8ed', border: true,
4623
4667
  borderColor: c.color || 'rgba(16,185,129,0.35)',