klypix-mcp 1.45.0 → 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 +2 -2
- package/src/brain-note.mjs +2 -1
- package/src/klypix-core.mjs +7 -2
- package/src/klypix-format.mjs +84 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "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",
|
package/src/brain-note.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// Optional trailing path picks a different .klypix (default ./brain.klypix).
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
|
-
import { captureIntoBrain, tidyBrain, atomicWrite, noteToCaptureInput } from './klypix-format.mjs';
|
|
22
|
+
import { captureIntoBrain, tidyBrain, atomicWrite, noteToCaptureInput, formatCaptureReceipts } from './klypix-format.mjs';
|
|
23
23
|
|
|
24
24
|
const MARKERS = { '': '', '?': '?', '!': '!', '✓': '✓', '~': '~', question: '?', milestone: '!', resolve: '✓', update: '~', decision: '', done: '✓' };
|
|
25
25
|
const normMarker = (m) => MARKERS[String(m || '').toLowerCase()] ?? '';
|
|
@@ -66,6 +66,7 @@ try {
|
|
|
66
66
|
const bits = [`${s.added || 0} added`];
|
|
67
67
|
for (const k of ['resolved', 'updated', 'closed', 'superseded', 'linked']) if (s[k]) bits.push(`${s[k]} ${k}`);
|
|
68
68
|
console.error(`✓ brain-note → ${path.basename(file)} (${bits.join(' · ')})`);
|
|
69
|
+
for (const line of formatCaptureReceipts(s)) console.error(` ${line}`);
|
|
69
70
|
} catch (e) {
|
|
70
71
|
console.error(`brain-note failed (brain unchanged): ${e?.message || e}`);
|
|
71
72
|
process.exit(1);
|
package/src/klypix-core.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
statusContextToMarkdown, findFulfillmentCandidates,
|
|
33
33
|
splitQueryTokens, scoreCardsAgainstQuery, correctionOverlaysFor,
|
|
34
34
|
isFastDecayCard, DECAY_STALE_MS, formatDecayAge,
|
|
35
|
-
readPendingShips, clearPendingShips, pendingShipCards,
|
|
35
|
+
readPendingShips, clearPendingShips, pendingShipCards, formatCaptureReceipts,
|
|
36
36
|
} from './klypix-format.mjs';
|
|
37
37
|
import { capMessages, findProjectBrain } from './agent-presence.mjs';
|
|
38
38
|
|
|
@@ -1031,7 +1031,12 @@ export async function opBrainNote({ vault, canvas, text: noteText, area, marker
|
|
|
1031
1031
|
: '';
|
|
1032
1032
|
// Name the resolved brain explicitly (basename + how) so a write never lands
|
|
1033
1033
|
// in a surprise file silently — the write-side twin of the read-op stamp.
|
|
1034
|
-
|
|
1034
|
+
// Host-neutral capture receipts (2026-08-01): Codex/Cursor/Cline capture
|
|
1035
|
+
// through THIS verb, not the Claude Stop hook — without these lines the
|
|
1036
|
+
// write-side ⏳/⚠️ nudges existed only on one host brand.
|
|
1037
|
+
const receipts = formatCaptureReceipts(s);
|
|
1038
|
+
const rc = receipts.length ? `\n${receipts.join('\n')}` : '';
|
|
1039
|
+
return { blocks: [text(`✓ brain_note → ${path.basename(file)} (via ${t.how}) · ${bits.join(' · ')}. Reopen the brain in the KLYPIX app to see it.${corr}${rc}`)] };
|
|
1035
1040
|
} catch (e) {
|
|
1036
1041
|
return err(`brain_note failed (brain unchanged): ${e.message}`);
|
|
1037
1042
|
}
|
package/src/klypix-format.mjs
CHANGED
|
@@ -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:
|
|
546
|
-
|
|
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);
|
|
@@ -4229,8 +4273,17 @@ const LIMITATION_CUE_RE = new RegExp([
|
|
|
4229
4273
|
/\bnot (?:implemented|wired|supported|available|built|shipped|published|deployed|enforced|persisted|possible|reachable|exposed|functional)\b/u.source,
|
|
4230
4274
|
/\breturns? (?:a )?(?:mock|stub|null|nothing)\b/u.source,
|
|
4231
4275
|
/\bis (?:dead|unused|missing|absent|unreachable|unwired|talk[- ]?only)\b/u.source,
|
|
4232
|
-
/\bonly (?:works|runs|streams|answers|covers|supports|reads)\b/u.source,
|
|
4276
|
+
/\bonly (?:works|runs|streams|answers|covers|supports|reads|checks|compares|matches|lowercases|normalizes)\b/u.source,
|
|
4233
4277
|
/\bstill (?:missing|absent|manual|unwired|blocked|mocked?)\b/u.source,
|
|
4278
|
+
// 2026-08-01, first real-world miss (the day after shipping): the presence-
|
|
4279
|
+
// bugs skill card asserted "compares hostPid ALONE", "only lowercases+slash-
|
|
4280
|
+
// normalizes" and "indistinguishable from success" — all state claims, none
|
|
4281
|
+
// matched a cue, so the card kept reading as settled law for a full session
|
|
4282
|
+
// after ab10688 had fixed every one of them. Cues grow ON-DEMAND from real
|
|
4283
|
+
// misses, never speculatively — each addition below names its incident.
|
|
4284
|
+
/\b(?:compares?|keys?|keyed|matches?) [\p{L}\p{N}_-]+ alone\b/u.source,
|
|
4285
|
+
/\bindistinguishable from\b/u.source,
|
|
4286
|
+
/\bsilently (?:miss(?:es|ed)?|fail(?:s|ed)?|drop(?:s|ped)?)\b/u.source,
|
|
4234
4287
|
].join('|'), 'iu');
|
|
4235
4288
|
// Imperative openers = advice, not state. Anchored to the CLAUSE start so
|
|
4236
4289
|
// "Chat has no tools" (subject-first) passes while "never set X" is refused.
|
|
@@ -4321,6 +4374,29 @@ export function findSkillObsolescenceCandidates(struct, milestones, { coverAt =
|
|
|
4321
4374
|
return capped;
|
|
4322
4375
|
}
|
|
4323
4376
|
|
|
4377
|
+
// ── Capture receipts, host-neutral (2026-08-01 parity fix) ──────────────────
|
|
4378
|
+
// The ⏳/⚠️ capture receipts used to print ONLY through the Claude Stop-hook's
|
|
4379
|
+
// stderr — a Codex / Cursor / Cline session capturing through brain_note (MCP
|
|
4380
|
+
// or CLI) got "1 added · 1 superseded" and never saw the nudge, so on those
|
|
4381
|
+
// hosts the write-side half of both detection classes was silently absent.
|
|
4382
|
+
// One formatter, consumed by opBrainNote (MCP) and brain-note (CLI), so every
|
|
4383
|
+
// host hears what the hook hears; the hook keeps its own prefix style.
|
|
4384
|
+
export function formatCaptureReceipts(stats, { maxEach = 3 } = {}) {
|
|
4385
|
+
const s = stats || {};
|
|
4386
|
+
const lines = [];
|
|
4387
|
+
for (const f of (Array.isArray(s.fulfillCandidates) ? s.fulfillCandidates : []).slice(0, maxEach)) {
|
|
4388
|
+
const rest = f.uncovered && f.uncovered.length ? ` · does NOT cover: ${f.uncovered.map(u => `"${String(u).slice(0, 50)}"`).join(', ')}` : '';
|
|
4389
|
+
const act = f.marker
|
|
4390
|
+
? `— if truly done, emit: ${f.marker}`
|
|
4391
|
+
: '— PARTIAL/short: the card stays open (verify by hand; dismiss via brain_connect relationship:"not_fulfilled")';
|
|
4392
|
+
lines.push(`⏳ likely fulfilled (${f.cov}): "${String(f.item).slice(0, 70)}"${rest} ${act}`);
|
|
4393
|
+
}
|
|
4394
|
+
for (const f of (Array.isArray(s.skillStale) ? s.skillStale : []).slice(0, maxEach)) {
|
|
4395
|
+
lines.push(`⚠️ rule may be obsolete (${f.cov}): skill "${String(f.skill).slice(0, 70)}" asserts "${String(f.clause || '').slice(0, 60)}" — this ship appears to remove it. If so, amend: ${f.marker} (retire by naming it in closes:, or dismiss via brain_connect relationship:"not_fulfilled")`);
|
|
4396
|
+
}
|
|
4397
|
+
return lines;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4324
4400
|
// Persisted-edge overlay reader for 'may obsolete' hints — the 🛠️ twin of
|
|
4325
4401
|
// fulfillmentOverlaysFor, same rules: dismissals win, a since-archived
|
|
4326
4402
|
// milestone no longer vouches, machine-written hints stay hedged forever.
|
|
@@ -4585,7 +4661,7 @@ export async function buildKlypixMap(spec) {
|
|
|
4585
4661
|
const id = `txt_${rand()}_${ai}_${ci}`;
|
|
4586
4662
|
const h = measured[ci];
|
|
4587
4663
|
items[id] = {
|
|
4588
|
-
type: 'text', locked: false, createdAt: now, createdBy: 'agent',
|
|
4664
|
+
type: 'text', locked: false, createdAt: now, createdBy: 'agent', ...authorField(),
|
|
4589
4665
|
content: String(c.text), fontSize: FONT,
|
|
4590
4666
|
color: c.color || '#e8e8ed', border: true,
|
|
4591
4667
|
borderColor: c.color || 'rgba(16,185,129,0.35)',
|