@productbrain/cli 0.1.0-beta.1 → 0.1.0-beta.14
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/README.md +167 -0
- package/dist/__tests__/audit.test.d.ts +2 -0
- package/dist/__tests__/audit.test.d.ts.map +1 -0
- package/dist/__tests__/audit.test.js +394 -0
- package/dist/__tests__/audit.test.js.map +1 -0
- package/dist/__tests__/capture.test.d.ts +2 -0
- package/dist/__tests__/capture.test.d.ts.map +1 -0
- package/dist/__tests__/capture.test.js +86 -0
- package/dist/__tests__/capture.test.js.map +1 -0
- package/dist/__tests__/constellation.test.d.ts +2 -0
- package/dist/__tests__/constellation.test.d.ts.map +1 -0
- package/dist/__tests__/constellation.test.js +260 -0
- package/dist/__tests__/constellation.test.js.map +1 -0
- package/dist/__tests__/context-strategy.test.d.ts +2 -0
- package/dist/__tests__/context-strategy.test.d.ts.map +1 -0
- package/dist/__tests__/context-strategy.test.js +79 -0
- package/dist/__tests__/context-strategy.test.js.map +1 -0
- package/dist/__tests__/fields.test.d.ts +2 -0
- package/dist/__tests__/fields.test.d.ts.map +1 -0
- package/dist/__tests__/fields.test.js +238 -0
- package/dist/__tests__/fields.test.js.map +1 -0
- package/dist/__tests__/handshake.test.d.ts +2 -0
- package/dist/__tests__/handshake.test.d.ts.map +1 -0
- package/dist/__tests__/handshake.test.js +187 -0
- package/dist/__tests__/handshake.test.js.map +1 -0
- package/dist/__tests__/ingest.test.d.ts +2 -0
- package/dist/__tests__/ingest.test.d.ts.map +1 -0
- package/dist/__tests__/ingest.test.js +185 -0
- package/dist/__tests__/ingest.test.js.map +1 -0
- package/dist/__tests__/promote.test.d.ts +2 -0
- package/dist/__tests__/promote.test.d.ts.map +1 -0
- package/dist/__tests__/promote.test.js +139 -0
- package/dist/__tests__/promote.test.js.map +1 -0
- package/dist/__tests__/proposals.test.d.ts +2 -0
- package/dist/__tests__/proposals.test.d.ts.map +1 -0
- package/dist/__tests__/proposals.test.js +190 -0
- package/dist/__tests__/proposals.test.js.map +1 -0
- package/dist/__tests__/relate.test.d.ts +2 -0
- package/dist/__tests__/relate.test.d.ts.map +1 -0
- package/dist/__tests__/relate.test.js +105 -0
- package/dist/__tests__/relate.test.js.map +1 -0
- package/dist/__tests__/repo-detect.test.d.ts +2 -0
- package/dist/__tests__/repo-detect.test.d.ts.map +1 -0
- package/dist/__tests__/repo-detect.test.js +119 -0
- package/dist/__tests__/repo-detect.test.js.map +1 -0
- package/dist/__tests__/runner.test.d.ts +2 -0
- package/dist/__tests__/runner.test.d.ts.map +1 -0
- package/dist/__tests__/runner.test.js +215 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/__tests__/session-touch.test.d.ts +2 -0
- package/dist/__tests__/session-touch.test.d.ts.map +1 -0
- package/dist/__tests__/session-touch.test.js +134 -0
- package/dist/__tests__/session-touch.test.js.map +1 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +52 -0
- package/dist/__tests__/session.test.js.map +1 -0
- package/dist/__tests__/strip.test.d.ts +2 -0
- package/dist/__tests__/strip.test.d.ts.map +1 -0
- package/dist/__tests__/strip.test.js +136 -0
- package/dist/__tests__/strip.test.js.map +1 -0
- package/dist/__tests__/update.test.d.ts +2 -0
- package/dist/__tests__/update.test.d.ts.map +1 -0
- package/dist/__tests__/update.test.js +237 -0
- package/dist/__tests__/update.test.js.map +1 -0
- package/dist/commands/accept.d.ts +18 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +72 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/audit.d.ts +25 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +188 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/brand-pack.d.ts +2 -0
- package/dist/commands/brand-pack.d.ts.map +1 -0
- package/dist/commands/brand-pack.js +25 -0
- package/dist/commands/brand-pack.js.map +1 -0
- package/dist/commands/brief.d.ts +28 -0
- package/dist/commands/brief.d.ts.map +1 -0
- package/dist/commands/brief.js +70 -0
- package/dist/commands/brief.js.map +1 -0
- package/dist/commands/capture.d.ts +21 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +100 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/chain-walk.d.ts +14 -0
- package/dist/commands/chain-walk.d.ts.map +1 -0
- package/dist/commands/chain-walk.js +33 -0
- package/dist/commands/chain-walk.js.map +1 -0
- package/dist/commands/changes.d.ts +11 -0
- package/dist/commands/changes.d.ts.map +1 -0
- package/dist/commands/changes.js +41 -0
- package/dist/commands/changes.js.map +1 -0
- package/dist/commands/constellation.d.ts +11 -0
- package/dist/commands/constellation.d.ts.map +1 -0
- package/dist/commands/constellation.js +28 -0
- package/dist/commands/constellation.js.map +1 -0
- package/dist/commands/context.d.ts +2 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +19 -9
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/cross-cut.d.ts +11 -0
- package/dist/commands/cross-cut.d.ts.map +1 -0
- package/dist/commands/cross-cut.js +23 -0
- package/dist/commands/cross-cut.js.map +1 -0
- package/dist/commands/fields.d.ts +9 -0
- package/dist/commands/fields.d.ts.map +1 -0
- package/dist/commands/fields.js +26 -0
- package/dist/commands/fields.js.map +1 -0
- package/dist/commands/get.d.ts +8 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +55 -6
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/handshake.d.ts +18 -0
- package/dist/commands/handshake.d.ts.map +1 -0
- package/dist/commands/handshake.js +378 -0
- package/dist/commands/handshake.js.map +1 -0
- package/dist/commands/ingest.d.ts +14 -0
- package/dist/commands/ingest.d.ts.map +1 -0
- package/dist/commands/ingest.js +181 -0
- package/dist/commands/ingest.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +53 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/orient.d.ts +2 -0
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +17 -8
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/promote.d.ts +12 -0
- package/dist/commands/promote.d.ts.map +1 -0
- package/dist/commands/promote.js +48 -0
- package/dist/commands/promote.js.map +1 -0
- package/dist/commands/proposals.d.ts +9 -0
- package/dist/commands/proposals.d.ts.map +1 -0
- package/dist/commands/proposals.js +24 -0
- package/dist/commands/proposals.js.map +1 -0
- package/dist/commands/reject.d.ts +14 -0
- package/dist/commands/reject.d.ts.map +1 -0
- package/dist/commands/reject.js +37 -0
- package/dist/commands/reject.js.map +1 -0
- package/dist/commands/relate.d.ts +16 -0
- package/dist/commands/relate.d.ts.map +1 -0
- package/dist/commands/relate.js +80 -0
- package/dist/commands/relate.js.map +1 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +9 -3
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +134 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +139 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/verify.d.ts +13 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +43 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/formatters/audit.d.ts +46 -0
- package/dist/formatters/audit.d.ts.map +1 -0
- package/dist/formatters/audit.js +81 -0
- package/dist/formatters/audit.js.map +1 -0
- package/dist/formatters/brief.d.ts +112 -0
- package/dist/formatters/brief.d.ts.map +1 -0
- package/dist/formatters/brief.js +179 -0
- package/dist/formatters/brief.js.map +1 -0
- package/dist/formatters/capture.d.ts +30 -0
- package/dist/formatters/capture.d.ts.map +1 -0
- package/dist/formatters/capture.js +58 -0
- package/dist/formatters/capture.js.map +1 -0
- package/dist/formatters/chain-walk.d.ts +33 -0
- package/dist/formatters/chain-walk.d.ts.map +1 -0
- package/dist/formatters/chain-walk.js +54 -0
- package/dist/formatters/chain-walk.js.map +1 -0
- package/dist/formatters/changes.d.ts +25 -0
- package/dist/formatters/changes.d.ts.map +1 -0
- package/dist/formatters/changes.js +60 -0
- package/dist/formatters/changes.js.map +1 -0
- package/dist/formatters/constellation.d.ts +34 -0
- package/dist/formatters/constellation.d.ts.map +1 -0
- package/dist/formatters/constellation.js +38 -0
- package/dist/formatters/constellation.js.map +1 -0
- package/dist/formatters/cross-cut.d.ts +21 -0
- package/dist/formatters/cross-cut.d.ts.map +1 -0
- package/dist/formatters/cross-cut.js +32 -0
- package/dist/formatters/cross-cut.js.map +1 -0
- package/dist/formatters/entry.d.ts +5 -0
- package/dist/formatters/entry.d.ts.map +1 -1
- package/dist/formatters/entry.js +5 -1
- package/dist/formatters/entry.js.map +1 -1
- package/dist/formatters/fields.d.ts +32 -0
- package/dist/formatters/fields.d.ts.map +1 -0
- package/dist/formatters/fields.js +49 -0
- package/dist/formatters/fields.js.map +1 -0
- package/dist/formatters/handshake.d.ts +17 -0
- package/dist/formatters/handshake.d.ts.map +1 -0
- package/dist/formatters/handshake.js +51 -0
- package/dist/formatters/handshake.js.map +1 -0
- package/dist/formatters/orient.d.ts +1 -0
- package/dist/formatters/orient.d.ts.map +1 -1
- package/dist/formatters/orient.js +4 -2
- package/dist/formatters/orient.js.map +1 -1
- package/dist/formatters/promote.d.ts +29 -0
- package/dist/formatters/promote.d.ts.map +1 -0
- package/dist/formatters/promote.js +38 -0
- package/dist/formatters/promote.js.map +1 -0
- package/dist/formatters/proposals.d.ts +45 -0
- package/dist/formatters/proposals.d.ts.map +1 -0
- package/dist/formatters/proposals.js +62 -0
- package/dist/formatters/proposals.js.map +1 -0
- package/dist/formatters/relate.d.ts +12 -0
- package/dist/formatters/relate.d.ts.map +1 -0
- package/dist/formatters/relate.js +13 -0
- package/dist/formatters/relate.js.map +1 -0
- package/dist/formatters/session.d.ts +11 -0
- package/dist/formatters/session.d.ts.map +1 -0
- package/dist/formatters/session.js +51 -0
- package/dist/formatters/session.js.map +1 -0
- package/dist/formatters/update.d.ts +17 -0
- package/dist/formatters/update.d.ts.map +1 -0
- package/dist/formatters/update.js +43 -0
- package/dist/formatters/update.js.map +1 -0
- package/dist/formatters/verify.d.ts +11 -0
- package/dist/formatters/verify.d.ts.map +1 -0
- package/dist/formatters/verify.js +11 -0
- package/dist/formatters/verify.js.map +1 -0
- package/dist/generators/adapters.d.ts +10 -0
- package/dist/generators/adapters.d.ts.map +1 -0
- package/dist/generators/adapters.js +102 -0
- package/dist/generators/adapters.js.map +1 -0
- package/dist/generators/briefing-md.d.ts +8 -0
- package/dist/generators/briefing-md.d.ts.map +1 -0
- package/dist/generators/briefing-md.js +51 -0
- package/dist/generators/briefing-md.js.map +1 -0
- package/dist/generators/context-md.d.ts +8 -0
- package/dist/generators/context-md.d.ts.map +1 -0
- package/dist/generators/context-md.js +123 -0
- package/dist/generators/context-md.js.map +1 -0
- package/dist/generators/portable-knowledge.d.ts +72 -0
- package/dist/generators/portable-knowledge.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.js +246 -0
- package/dist/generators/portable-knowledge.js.map +1 -0
- package/dist/generators/portable-knowledge.test.d.ts +2 -0
- package/dist/generators/portable-knowledge.test.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.test.js +399 -0
- package/dist/generators/portable-knowledge.test.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +462 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +34 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +114 -9
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +19 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +95 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/repo-detect.d.ts +14 -0
- package/dist/lib/repo-detect.d.ts.map +1 -0
- package/dist/lib/repo-detect.js +58 -0
- package/dist/lib/repo-detect.js.map +1 -0
- package/dist/lib/runner.d.ts +31 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +65 -0
- package/dist/lib/runner.js.map +1 -0
- package/dist/lib/session.d.ts +17 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +43 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/strip.d.ts +11 -0
- package/dist/lib/strip.d.ts.map +1 -0
- package/dist/lib/strip.js +26 -0
- package/dist/lib/strip.js.map +1 -0
- package/package.json +8 -4
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable knowledge — reads canonical skills/rules from .productbrain/
|
|
3
|
+
* and generates tool-specific copies for Cursor, Claude Code, etc.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join, basename } from 'path';
|
|
7
|
+
import { MARKER } from './adapters.js';
|
|
8
|
+
/** Returns true if the entry should be emitted to the given target. */
|
|
9
|
+
export function shouldEmitToTarget(entry, target) {
|
|
10
|
+
if (!entry.targets)
|
|
11
|
+
return true;
|
|
12
|
+
return entry.targets.includes(target);
|
|
13
|
+
}
|
|
14
|
+
const LEVEL_HIERARCHY = {
|
|
15
|
+
beginner: ['core'],
|
|
16
|
+
intermediate: ['core', 'intermediate'],
|
|
17
|
+
expert: ['core', 'intermediate', 'expert'],
|
|
18
|
+
};
|
|
19
|
+
const VALID_LEVELS = new Set(Object.keys(LEVEL_HIERARCHY));
|
|
20
|
+
/**
|
|
21
|
+
* Filter items by graduated level.
|
|
22
|
+
* - If requestedLevel is undefined/null: return ALL items (backward compat).
|
|
23
|
+
* - Items with no `level` field: always included (backward compat).
|
|
24
|
+
* - Otherwise: include items whose level is within the hierarchy for requestedLevel.
|
|
25
|
+
*/
|
|
26
|
+
export function filterByLevel(items, requestedLevel) {
|
|
27
|
+
if (!requestedLevel)
|
|
28
|
+
return items;
|
|
29
|
+
if (!VALID_LEVELS.has(requestedLevel)) {
|
|
30
|
+
throw new Error(`Unknown level "${requestedLevel}". Valid levels: ${[...VALID_LEVELS].join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
const allowedLevels = LEVEL_HIERARCHY[requestedLevel];
|
|
33
|
+
return items.filter((item) => !item.level || allowedLevels.includes(item.level));
|
|
34
|
+
}
|
|
35
|
+
// ── Frontmatter parser ─────────────────────────────────────────────────
|
|
36
|
+
function parseFrontmatter(raw) {
|
|
37
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
38
|
+
if (!match)
|
|
39
|
+
return { fields: new Map(), arrayFields: new Map(), body: raw };
|
|
40
|
+
const yaml = match[1];
|
|
41
|
+
const body = match[2];
|
|
42
|
+
const fields = new Map();
|
|
43
|
+
const arrayFields = new Map();
|
|
44
|
+
const lines = yaml.split('\n');
|
|
45
|
+
let currentKey = null;
|
|
46
|
+
let collectingMultiline = false;
|
|
47
|
+
let collectingArray = false;
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
// Array item: " - value"
|
|
50
|
+
if (collectingArray && currentKey && /^\s+-\s/.test(line)) {
|
|
51
|
+
const items = arrayFields.get(currentKey) ?? [];
|
|
52
|
+
items.push(line.replace(/^\s+-\s+/, '').trim().replace(/^["']|["']$/g, ''));
|
|
53
|
+
arrayFields.set(currentKey, items);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Multiline continuation: indented text after >- or >
|
|
57
|
+
if (collectingMultiline && currentKey && /^\s+\S/.test(line)) {
|
|
58
|
+
const existing = fields.get(currentKey) ?? '';
|
|
59
|
+
fields.set(currentKey, (existing ? existing + ' ' : '') + line.trim());
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// New key — reset collection state
|
|
63
|
+
collectingMultiline = false;
|
|
64
|
+
collectingArray = false;
|
|
65
|
+
const kv = line.match(/^([\w-]+):\s*(.*)/);
|
|
66
|
+
if (!kv)
|
|
67
|
+
continue;
|
|
68
|
+
currentKey = kv[1];
|
|
69
|
+
const val = kv[2].trim();
|
|
70
|
+
if (val === '' || val === '>-' || val === '>') {
|
|
71
|
+
// Could be array or multiline — peek ahead via next iterations
|
|
72
|
+
// If next line starts with " -", it's an array; if indented text, it's multiline
|
|
73
|
+
collectingMultiline = val === '>-' || val === '>';
|
|
74
|
+
collectingArray = val === '';
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
fields.set(currentKey, val.replace(/^["']|["']$/g, ''));
|
|
78
|
+
}
|
|
79
|
+
return { fields, arrayFields, body };
|
|
80
|
+
}
|
|
81
|
+
// ── Readers ────────────────────────────────────────────────────────────
|
|
82
|
+
export function readCanonicalSkills(productbrainDir) {
|
|
83
|
+
const skillsDir = join(productbrainDir, 'skills');
|
|
84
|
+
if (!existsSync(skillsDir))
|
|
85
|
+
return [];
|
|
86
|
+
const files = readdirSync(skillsDir).filter((f) => f.endsWith('.md'));
|
|
87
|
+
const skills = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const filePath = join(skillsDir, file);
|
|
90
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
91
|
+
const { fields, arrayFields, body } = parseFrontmatter(raw);
|
|
92
|
+
const name = fields.get('name') ?? basename(file, '.md');
|
|
93
|
+
const description = fields.get('description') ?? '';
|
|
94
|
+
const triggers = arrayFields.get('triggers') ?? [];
|
|
95
|
+
const targets = arrayFields.get('targets');
|
|
96
|
+
const level = fields.get('level');
|
|
97
|
+
skills.push({ name, description, triggers, body, sourcePath: filePath, targets: targets?.length ? targets : undefined, level: level || undefined });
|
|
98
|
+
}
|
|
99
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
}
|
|
101
|
+
export function readCanonicalRules(productbrainDir) {
|
|
102
|
+
const rulesDir = join(productbrainDir, 'rules');
|
|
103
|
+
if (!existsSync(rulesDir))
|
|
104
|
+
return [];
|
|
105
|
+
const files = readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
|
|
106
|
+
const rules = [];
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const filePath = join(rulesDir, file);
|
|
109
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
110
|
+
const { fields, arrayFields, body } = parseFrontmatter(raw);
|
|
111
|
+
const name = fields.get('name') ?? basename(file, '.md');
|
|
112
|
+
const description = fields.get('description') ?? '';
|
|
113
|
+
const scope = fields.get('scope');
|
|
114
|
+
const autoApply = fields.get('autoApply') === 'true';
|
|
115
|
+
const targets = arrayFields.get('targets');
|
|
116
|
+
const level = fields.get('level');
|
|
117
|
+
rules.push({ name, description, scope, autoApply, body, sourcePath: filePath, targets: targets?.length ? targets : undefined, level: level || undefined });
|
|
118
|
+
}
|
|
119
|
+
return rules.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
|
+
}
|
|
121
|
+
// ── Transport section stripping ───────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Strip transport-conditional sections from a skill body.
|
|
124
|
+
*
|
|
125
|
+
* Sections are delimited by HTML comments:
|
|
126
|
+
* <!-- transport:TARGET_NAME -->
|
|
127
|
+
* ...content...
|
|
128
|
+
* <!-- /transport -->
|
|
129
|
+
*
|
|
130
|
+
* - Blocks matching the current target: keep the content, remove the delimiter comments.
|
|
131
|
+
* - Blocks for OTHER targets: remove entirely (delimiters + content).
|
|
132
|
+
* - Content outside any transport block: keep as-is (universal content).
|
|
133
|
+
*/
|
|
134
|
+
export function stripTransportSections(body, target) {
|
|
135
|
+
// Match transport blocks: <!-- transport:NAME -->...<!-- /transport -->
|
|
136
|
+
// Using a non-greedy match between the opening and the NEXT closing tag.
|
|
137
|
+
const transportBlockRe = /<!-- transport:(\w+) -->\n?([\s\S]*?)<!-- \/transport -->\n?/g;
|
|
138
|
+
return body.replace(transportBlockRe, (_match, blockTarget, content) => {
|
|
139
|
+
if (blockTarget === target) {
|
|
140
|
+
// Keep the content, remove the delimiter comments
|
|
141
|
+
return content;
|
|
142
|
+
}
|
|
143
|
+
// Remove the entire block for other targets
|
|
144
|
+
return '';
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// ── Cursor generators ──────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Generate a Cursor SKILL.md from a canonical skill.
|
|
150
|
+
* Cursor expects: name, description (with triggers embedded).
|
|
151
|
+
*/
|
|
152
|
+
export function generateCursorSkill(skill) {
|
|
153
|
+
// Cursor puts trigger phrases in the description field
|
|
154
|
+
const triggerLine = skill.triggers.length > 0
|
|
155
|
+
? ` Use when the user says ${skill.triggers.map((t) => `"${t}"`).join(', ')}.`
|
|
156
|
+
: '';
|
|
157
|
+
const description = skill.description + triggerLine;
|
|
158
|
+
// Strip transport sections BEFORE path replacement
|
|
159
|
+
const stripped = stripTransportSections(skill.body, 'cursor');
|
|
160
|
+
// Replace canonical paths with Cursor paths in body
|
|
161
|
+
const body = stripped
|
|
162
|
+
.replace(/\.productbrain\/rules\/(\S+)\.md/g, '.cursor/rules/$1.mdc')
|
|
163
|
+
.replace(/\.productbrain\/skills\/(\S+)\.md/g, '.cursor/skills/$1/SKILL.md');
|
|
164
|
+
return `---
|
|
165
|
+
name: ${skill.name}
|
|
166
|
+
description: >-
|
|
167
|
+
${description.replace(/\n/g, '\n ')}
|
|
168
|
+
---
|
|
169
|
+
<!-- ${MARKER} — source: .productbrain/skills/${skill.name}.md -->
|
|
170
|
+
|
|
171
|
+
${body}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Generate a Cursor .mdc rule from a canonical rule.
|
|
175
|
+
* Cursor expects: description, globs, alwaysApply.
|
|
176
|
+
*/
|
|
177
|
+
export function generateCursorRule(rule) {
|
|
178
|
+
// Replace canonical paths with Cursor paths in body
|
|
179
|
+
const body = rule.body
|
|
180
|
+
.replace(/\.productbrain\/rules\/(\S+)\.md/g, '.cursor/rules/$1.mdc')
|
|
181
|
+
.replace(/\.productbrain\/skills\/(\S+)\.md/g, '.cursor/skills/$1/SKILL.md');
|
|
182
|
+
return `---
|
|
183
|
+
description: ${rule.description}
|
|
184
|
+
globs: ${rule.scope ?? ''}
|
|
185
|
+
alwaysApply: ${rule.autoApply}
|
|
186
|
+
---
|
|
187
|
+
<!-- ${MARKER} — source: .productbrain/rules/${rule.name}.md -->
|
|
188
|
+
|
|
189
|
+
${body}`;
|
|
190
|
+
}
|
|
191
|
+
// ── Claude Code generators ────────────────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Generate a Claude Code .md rule from a canonical rule.
|
|
194
|
+
* Claude Code expects: description, optional paths (array).
|
|
195
|
+
*/
|
|
196
|
+
export function generateClaudeRule(rule) {
|
|
197
|
+
// Replace canonical paths with Claude Code paths in body
|
|
198
|
+
const body = rule.body
|
|
199
|
+
.replace(/\.productbrain\/rules\/(\S+)\.md/g, '.claude/rules/$1.md')
|
|
200
|
+
.replace(/\.productbrain\/skills\/(\S+)\.md/g, '.productbrain/skills/$1.md');
|
|
201
|
+
// Build frontmatter
|
|
202
|
+
const fmLines = ['---'];
|
|
203
|
+
fmLines.push(`description: "${rule.description.replace(/"/g, '\\"')}"`);
|
|
204
|
+
if (rule.scope) {
|
|
205
|
+
fmLines.push('paths:');
|
|
206
|
+
fmLines.push(` - "${rule.scope}"`);
|
|
207
|
+
}
|
|
208
|
+
fmLines.push('---');
|
|
209
|
+
return `${fmLines.join('\n')}
|
|
210
|
+
<!-- ${MARKER} — source: .productbrain/rules/${rule.name}.md -->
|
|
211
|
+
|
|
212
|
+
${body}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generate a Claude Code skill-router.md — imperative skill activation rule.
|
|
216
|
+
* Loaded automatically by Claude Code via .claude/rules/ with no paths (always active).
|
|
217
|
+
*/
|
|
218
|
+
export function generateClaudeSkillRouter(skills) {
|
|
219
|
+
if (skills.length === 0)
|
|
220
|
+
return '';
|
|
221
|
+
const triggerBlocks = skills.map((skill) => {
|
|
222
|
+
const triggerList = skill.triggers.map((t) => `"${t}"`).join(' / ');
|
|
223
|
+
return `- User says ${triggerList}
|
|
224
|
+
→ READ \`.productbrain/skills/${skill.name}.md\` — follow its full protocol`;
|
|
225
|
+
});
|
|
226
|
+
return `---
|
|
227
|
+
description: "Skill activation router — MUST read skill file before responding when triggers match"
|
|
228
|
+
---
|
|
229
|
+
<!-- ${MARKER} — source: generated from .productbrain/skills/*.md -->
|
|
230
|
+
|
|
231
|
+
# Skill Router — Mandatory Activation
|
|
232
|
+
|
|
233
|
+
BEFORE responding to ANY user message, scan for these triggers.
|
|
234
|
+
If ANY trigger matches: READ the skill file FIRST, then follow its full protocol.
|
|
235
|
+
Do NOT respond with your own approach. The skill IS the approach.
|
|
236
|
+
|
|
237
|
+
## Triggers
|
|
238
|
+
|
|
239
|
+
${triggerBlocks.join('\n\n')}
|
|
240
|
+
|
|
241
|
+
## Why This Matters
|
|
242
|
+
|
|
243
|
+
Skipping the skill file produces a shallow response that misses governance, blast radius, and Chain context. This is the #1 quality failure in this workspace. The skill files encode the full protocol — including Chain checks, domain identification, and review gates — that a direct response will miss.
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=portable-knowledge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-knowledge.js","sourceRoot":"","sources":["../../src/generators/portable-knowledge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-knowledge.test.d.ts","sourceRoot":"","sources":["../../src/generators/portable-knowledge.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* portable-knowledge — unit tests.
|
|
3
|
+
* BET-169: transport-aware skill dispatch, target filtering, and transport section stripping.
|
|
4
|
+
*/
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
// vi.mock calls are hoisted — use vi.hoisted() for constants referenced inside factories.
|
|
8
|
+
const { vfs } = vi.hoisted(() => ({
|
|
9
|
+
vfs: {},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('fs', () => ({
|
|
12
|
+
mkdirSync: vi.fn(),
|
|
13
|
+
writeFileSync: vi.fn((path, content) => {
|
|
14
|
+
vfs[path] = content;
|
|
15
|
+
}),
|
|
16
|
+
existsSync: vi.fn((path) => {
|
|
17
|
+
// Check if the path itself or any child key exists (for directory checks)
|
|
18
|
+
if (path in vfs)
|
|
19
|
+
return true;
|
|
20
|
+
// For directory checks: return true if any key starts with path + '/'
|
|
21
|
+
return Object.keys(vfs).some((k) => k.startsWith(path + '/'));
|
|
22
|
+
}),
|
|
23
|
+
readFileSync: vi.fn((path, _enc) => {
|
|
24
|
+
if (path in vfs)
|
|
25
|
+
return vfs[path];
|
|
26
|
+
throw Object.assign(new Error(`ENOENT: no such file '${path}'`), { code: 'ENOENT' });
|
|
27
|
+
}),
|
|
28
|
+
readdirSync: vi.fn((dir) => {
|
|
29
|
+
const prefix = dir.endsWith('/') ? dir : dir + '/';
|
|
30
|
+
const files = new Set();
|
|
31
|
+
for (const key of Object.keys(vfs)) {
|
|
32
|
+
if (key.startsWith(prefix)) {
|
|
33
|
+
const rest = key.slice(prefix.length);
|
|
34
|
+
const parts = rest.split('/');
|
|
35
|
+
if (parts.length === 1)
|
|
36
|
+
files.add(parts[0]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...files];
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
import { readCanonicalSkills, shouldEmitToTarget, stripTransportSections, filterByLevel, generateCursorSkill, generateClaudeSkillRouter, } from './portable-knowledge.js';
|
|
43
|
+
const PB_DIR = '/tmp/pb-test/.productbrain';
|
|
44
|
+
describe('readCanonicalSkills', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
Object.keys(vfs).forEach((k) => delete vfs[k]);
|
|
47
|
+
});
|
|
48
|
+
it('parses targets from frontmatter', () => {
|
|
49
|
+
vfs[join(PB_DIR, 'skills', 'test-skill.md')] = `---
|
|
50
|
+
name: test-skill
|
|
51
|
+
description: A test skill
|
|
52
|
+
triggers:
|
|
53
|
+
- test
|
|
54
|
+
targets:
|
|
55
|
+
- claude
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Test Skill Body
|
|
59
|
+
`;
|
|
60
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
61
|
+
expect(skills).toHaveLength(1);
|
|
62
|
+
expect(skills[0].targets).toEqual(['claude']);
|
|
63
|
+
});
|
|
64
|
+
it('returns undefined targets when not specified in frontmatter', () => {
|
|
65
|
+
vfs[join(PB_DIR, 'skills', 'universal-skill.md')] = `---
|
|
66
|
+
name: universal-skill
|
|
67
|
+
description: A universal skill
|
|
68
|
+
triggers:
|
|
69
|
+
- universal
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# Universal Skill Body
|
|
73
|
+
`;
|
|
74
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
75
|
+
expect(skills).toHaveLength(1);
|
|
76
|
+
expect(skills[0].targets).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
it('parses multiple targets', () => {
|
|
79
|
+
vfs[join(PB_DIR, 'skills', 'multi-target.md')] = `---
|
|
80
|
+
name: multi-target
|
|
81
|
+
description: Multi-target skill
|
|
82
|
+
triggers:
|
|
83
|
+
- multi
|
|
84
|
+
targets:
|
|
85
|
+
- claude
|
|
86
|
+
- cursor
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Multi-Target Body
|
|
90
|
+
`;
|
|
91
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
92
|
+
expect(skills).toHaveLength(1);
|
|
93
|
+
expect(skills[0].targets).toEqual(['claude', 'cursor']);
|
|
94
|
+
});
|
|
95
|
+
it('parses level from frontmatter', () => {
|
|
96
|
+
vfs[join(PB_DIR, 'skills', 'leveled-skill.md')] = `---
|
|
97
|
+
name: leveled-skill
|
|
98
|
+
description: A leveled skill
|
|
99
|
+
level: core
|
|
100
|
+
triggers:
|
|
101
|
+
- leveled
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# Leveled Skill Body
|
|
105
|
+
`;
|
|
106
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
107
|
+
expect(skills).toHaveLength(1);
|
|
108
|
+
expect(skills[0].level).toBe('core');
|
|
109
|
+
});
|
|
110
|
+
it('returns undefined level when not specified in frontmatter', () => {
|
|
111
|
+
vfs[join(PB_DIR, 'skills', 'no-level-skill.md')] = `---
|
|
112
|
+
name: no-level-skill
|
|
113
|
+
description: A skill without level
|
|
114
|
+
triggers:
|
|
115
|
+
- nolevel
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# No Level Skill Body
|
|
119
|
+
`;
|
|
120
|
+
const skills = readCanonicalSkills(PB_DIR);
|
|
121
|
+
expect(skills).toHaveLength(1);
|
|
122
|
+
expect(skills[0].level).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('shouldEmitToTarget', () => {
|
|
126
|
+
it('returns true when targets is undefined (emit to all)', () => {
|
|
127
|
+
const skill = {
|
|
128
|
+
name: 'test',
|
|
129
|
+
description: '',
|
|
130
|
+
triggers: [],
|
|
131
|
+
body: '',
|
|
132
|
+
sourcePath: '',
|
|
133
|
+
};
|
|
134
|
+
expect(shouldEmitToTarget(skill, 'claude')).toBe(true);
|
|
135
|
+
expect(shouldEmitToTarget(skill, 'cursor')).toBe(true);
|
|
136
|
+
expect(shouldEmitToTarget(skill, 'copilot')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('returns true when target is in the list', () => {
|
|
139
|
+
const skill = {
|
|
140
|
+
name: 'test',
|
|
141
|
+
description: '',
|
|
142
|
+
triggers: [],
|
|
143
|
+
body: '',
|
|
144
|
+
sourcePath: '',
|
|
145
|
+
targets: ['claude'],
|
|
146
|
+
};
|
|
147
|
+
expect(shouldEmitToTarget(skill, 'claude')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
it('returns false when target is not in the list', () => {
|
|
150
|
+
const skill = {
|
|
151
|
+
name: 'test',
|
|
152
|
+
description: '',
|
|
153
|
+
triggers: [],
|
|
154
|
+
body: '',
|
|
155
|
+
sourcePath: '',
|
|
156
|
+
targets: ['claude'],
|
|
157
|
+
};
|
|
158
|
+
expect(shouldEmitToTarget(skill, 'cursor')).toBe(false);
|
|
159
|
+
expect(shouldEmitToTarget(skill, 'copilot')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('works for CanonicalRule as well', () => {
|
|
162
|
+
const rule = {
|
|
163
|
+
name: 'test-rule',
|
|
164
|
+
description: '',
|
|
165
|
+
autoApply: true,
|
|
166
|
+
body: '',
|
|
167
|
+
sourcePath: '',
|
|
168
|
+
targets: ['cursor'],
|
|
169
|
+
};
|
|
170
|
+
expect(shouldEmitToTarget(rule, 'cursor')).toBe(true);
|
|
171
|
+
expect(shouldEmitToTarget(rule, 'claude')).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('stripTransportSections', () => {
|
|
175
|
+
it('keeps content for the matching target and removes delimiters', () => {
|
|
176
|
+
const body = `Universal content above.
|
|
177
|
+
|
|
178
|
+
<!-- transport:claude -->
|
|
179
|
+
Claude-specific instructions here.
|
|
180
|
+
<!-- /transport -->
|
|
181
|
+
|
|
182
|
+
Universal content below.`;
|
|
183
|
+
const result = stripTransportSections(body, 'claude');
|
|
184
|
+
expect(result).toContain('Claude-specific instructions here.');
|
|
185
|
+
expect(result).toContain('Universal content above.');
|
|
186
|
+
expect(result).toContain('Universal content below.');
|
|
187
|
+
expect(result).not.toContain('<!-- transport:claude -->');
|
|
188
|
+
expect(result).not.toContain('<!-- /transport -->');
|
|
189
|
+
});
|
|
190
|
+
it('removes blocks for other targets entirely', () => {
|
|
191
|
+
const body = `Universal content.
|
|
192
|
+
|
|
193
|
+
<!-- transport:cursor -->
|
|
194
|
+
Cursor-only instructions.
|
|
195
|
+
<!-- /transport -->
|
|
196
|
+
|
|
197
|
+
More universal.`;
|
|
198
|
+
const result = stripTransportSections(body, 'claude');
|
|
199
|
+
expect(result).toContain('Universal content.');
|
|
200
|
+
expect(result).toContain('More universal.');
|
|
201
|
+
expect(result).not.toContain('Cursor-only instructions.');
|
|
202
|
+
expect(result).not.toContain('<!-- transport:cursor -->');
|
|
203
|
+
});
|
|
204
|
+
it('handles multiple transport blocks for different targets', () => {
|
|
205
|
+
const body = `# Heading
|
|
206
|
+
|
|
207
|
+
<!-- transport:claude -->
|
|
208
|
+
Claude dispatch: use Agent tool.
|
|
209
|
+
<!-- /transport -->
|
|
210
|
+
|
|
211
|
+
<!-- transport:cursor -->
|
|
212
|
+
Cursor dispatch: use fresh conversation.
|
|
213
|
+
<!-- /transport -->
|
|
214
|
+
|
|
215
|
+
## Footer`;
|
|
216
|
+
const claudeResult = stripTransportSections(body, 'claude');
|
|
217
|
+
expect(claudeResult).toContain('Claude dispatch: use Agent tool.');
|
|
218
|
+
expect(claudeResult).not.toContain('Cursor dispatch: use fresh conversation.');
|
|
219
|
+
expect(claudeResult).toContain('# Heading');
|
|
220
|
+
expect(claudeResult).toContain('## Footer');
|
|
221
|
+
const cursorResult = stripTransportSections(body, 'cursor');
|
|
222
|
+
expect(cursorResult).toContain('Cursor dispatch: use fresh conversation.');
|
|
223
|
+
expect(cursorResult).not.toContain('Claude dispatch: use Agent tool.');
|
|
224
|
+
expect(cursorResult).toContain('# Heading');
|
|
225
|
+
expect(cursorResult).toContain('## Footer');
|
|
226
|
+
});
|
|
227
|
+
it('keeps universal content untouched when no transport blocks exist', () => {
|
|
228
|
+
const body = `# Just a normal skill
|
|
229
|
+
|
|
230
|
+
No transport sections here.
|
|
231
|
+
|
|
232
|
+
## Section 2
|
|
233
|
+
|
|
234
|
+
More content.`;
|
|
235
|
+
const result = stripTransportSections(body, 'claude');
|
|
236
|
+
expect(result).toBe(body);
|
|
237
|
+
});
|
|
238
|
+
it('handles markdown and code blocks inside transport sections', () => {
|
|
239
|
+
const body = `Universal.
|
|
240
|
+
|
|
241
|
+
<!-- transport:claude -->
|
|
242
|
+
**Bold text** and \`inline code\`.
|
|
243
|
+
|
|
244
|
+
\`\`\`bash
|
|
245
|
+
git diff main..HEAD
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
- List item 1
|
|
249
|
+
- List item 2
|
|
250
|
+
<!-- /transport -->
|
|
251
|
+
|
|
252
|
+
End.`;
|
|
253
|
+
const result = stripTransportSections(body, 'claude');
|
|
254
|
+
expect(result).toContain('**Bold text** and `inline code`.');
|
|
255
|
+
expect(result).toContain('git diff main..HEAD');
|
|
256
|
+
expect(result).toContain('- List item 1');
|
|
257
|
+
expect(result).toContain('End.');
|
|
258
|
+
});
|
|
259
|
+
it('handles transport sections with no trailing newline after closing tag', () => {
|
|
260
|
+
const body = `Before.
|
|
261
|
+
<!-- transport:claude -->
|
|
262
|
+
Content.
|
|
263
|
+
<!-- /transport -->After.`;
|
|
264
|
+
const result = stripTransportSections(body, 'claude');
|
|
265
|
+
expect(result).toContain('Content.');
|
|
266
|
+
expect(result).toContain('After.');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('generateCursorSkill (transport stripping)', () => {
|
|
270
|
+
it('strips non-cursor transport sections from generated output', () => {
|
|
271
|
+
const skill = {
|
|
272
|
+
name: 'test-skill',
|
|
273
|
+
description: 'Test skill',
|
|
274
|
+
triggers: ['test'],
|
|
275
|
+
body: `# Heading
|
|
276
|
+
|
|
277
|
+
<!-- transport:claude -->
|
|
278
|
+
Claude-only content.
|
|
279
|
+
<!-- /transport -->
|
|
280
|
+
|
|
281
|
+
<!-- transport:cursor -->
|
|
282
|
+
Cursor-only content.
|
|
283
|
+
<!-- /transport -->
|
|
284
|
+
|
|
285
|
+
Universal content.`,
|
|
286
|
+
sourcePath: '/tmp/.productbrain/skills/test-skill.md',
|
|
287
|
+
};
|
|
288
|
+
const output = generateCursorSkill(skill);
|
|
289
|
+
expect(output).toContain('Cursor-only content.');
|
|
290
|
+
expect(output).not.toContain('Claude-only content.');
|
|
291
|
+
expect(output).toContain('Universal content.');
|
|
292
|
+
expect(output).not.toContain('<!-- transport:');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
describe('generateClaudeSkillRouter (target filtering)', () => {
|
|
296
|
+
it('includes only skills with matching or no targets', () => {
|
|
297
|
+
const claudeOnly = {
|
|
298
|
+
name: 'claude-skill',
|
|
299
|
+
description: 'Claude only',
|
|
300
|
+
triggers: ['test-claude'],
|
|
301
|
+
body: '# Claude',
|
|
302
|
+
sourcePath: '/tmp/skills/claude-skill.md',
|
|
303
|
+
targets: ['claude'],
|
|
304
|
+
};
|
|
305
|
+
const cursorOnly = {
|
|
306
|
+
name: 'cursor-skill',
|
|
307
|
+
description: 'Cursor only',
|
|
308
|
+
triggers: ['test-cursor'],
|
|
309
|
+
body: '# Cursor',
|
|
310
|
+
sourcePath: '/tmp/skills/cursor-skill.md',
|
|
311
|
+
targets: ['cursor'],
|
|
312
|
+
};
|
|
313
|
+
const universal = {
|
|
314
|
+
name: 'universal-skill',
|
|
315
|
+
description: 'Universal',
|
|
316
|
+
triggers: ['test-universal'],
|
|
317
|
+
body: '# Universal',
|
|
318
|
+
sourcePath: '/tmp/skills/universal-skill.md',
|
|
319
|
+
};
|
|
320
|
+
// Filter skills for Claude (as handshake.ts would do)
|
|
321
|
+
const claudeSkills = [claudeOnly, cursorOnly, universal].filter((s) => shouldEmitToTarget(s, 'claude'));
|
|
322
|
+
const router = generateClaudeSkillRouter(claudeSkills);
|
|
323
|
+
expect(router).toContain('claude-skill');
|
|
324
|
+
expect(router).toContain('universal-skill');
|
|
325
|
+
expect(router).not.toContain('cursor-skill');
|
|
326
|
+
});
|
|
327
|
+
it('skill with targets: [claude] does not appear in Cursor output', () => {
|
|
328
|
+
const claudeOnly = {
|
|
329
|
+
name: 'claude-exclusive',
|
|
330
|
+
description: 'Claude exclusive skill',
|
|
331
|
+
triggers: ['claude-trigger'],
|
|
332
|
+
body: '# Claude Exclusive',
|
|
333
|
+
sourcePath: '/tmp/skills/claude-exclusive.md',
|
|
334
|
+
targets: ['claude'],
|
|
335
|
+
};
|
|
336
|
+
// Filter for Cursor (as handshake.ts would do)
|
|
337
|
+
const cursorSkills = [claudeOnly].filter((s) => shouldEmitToTarget(s, 'cursor'));
|
|
338
|
+
expect(cursorSkills).toHaveLength(0);
|
|
339
|
+
});
|
|
340
|
+
it('skill with no targets appears in both Claude and Cursor output', () => {
|
|
341
|
+
const universal = {
|
|
342
|
+
name: 'universal',
|
|
343
|
+
description: 'Universal skill',
|
|
344
|
+
triggers: ['uni'],
|
|
345
|
+
body: '# Universal',
|
|
346
|
+
sourcePath: '/tmp/skills/universal.md',
|
|
347
|
+
};
|
|
348
|
+
const claudeSkills = [universal].filter((s) => shouldEmitToTarget(s, 'claude'));
|
|
349
|
+
const cursorSkills = [universal].filter((s) => shouldEmitToTarget(s, 'cursor'));
|
|
350
|
+
expect(claudeSkills).toHaveLength(1);
|
|
351
|
+
expect(cursorSkills).toHaveLength(1);
|
|
352
|
+
const router = generateClaudeSkillRouter(claudeSkills);
|
|
353
|
+
expect(router).toContain('universal');
|
|
354
|
+
const cursorOutput = generateCursorSkill(cursorSkills[0]);
|
|
355
|
+
expect(cursorOutput).toContain('universal');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
describe('filterByLevel', () => {
|
|
359
|
+
const items = [
|
|
360
|
+
{ name: 'core-item', level: 'core' },
|
|
361
|
+
{ name: 'intermediate-item', level: 'intermediate' },
|
|
362
|
+
{ name: 'expert-item', level: 'expert' },
|
|
363
|
+
{ name: 'no-level-item' },
|
|
364
|
+
];
|
|
365
|
+
it('filterByLevel("beginner") returns only level:core items + items with no level', () => {
|
|
366
|
+
const result = filterByLevel(items, 'beginner');
|
|
367
|
+
const names = result.map((i) => i.name);
|
|
368
|
+
expect(names).toContain('core-item');
|
|
369
|
+
expect(names).toContain('no-level-item');
|
|
370
|
+
expect(names).not.toContain('intermediate-item');
|
|
371
|
+
expect(names).not.toContain('expert-item');
|
|
372
|
+
expect(result).toHaveLength(2);
|
|
373
|
+
});
|
|
374
|
+
it('filterByLevel("intermediate") returns core + intermediate + no level', () => {
|
|
375
|
+
const result = filterByLevel(items, 'intermediate');
|
|
376
|
+
const names = result.map((i) => i.name);
|
|
377
|
+
expect(names).toContain('core-item');
|
|
378
|
+
expect(names).toContain('intermediate-item');
|
|
379
|
+
expect(names).toContain('no-level-item');
|
|
380
|
+
expect(names).not.toContain('expert-item');
|
|
381
|
+
expect(result).toHaveLength(3);
|
|
382
|
+
});
|
|
383
|
+
it('filterByLevel("expert") returns all items', () => {
|
|
384
|
+
const result = filterByLevel(items, 'expert');
|
|
385
|
+
expect(result).toHaveLength(4);
|
|
386
|
+
});
|
|
387
|
+
it('filterByLevel(undefined) returns all items (backward compat)', () => {
|
|
388
|
+
const result = filterByLevel(items, undefined);
|
|
389
|
+
expect(result).toHaveLength(4);
|
|
390
|
+
});
|
|
391
|
+
it('filterByLevel(null-ish) returns all items (backward compat)', () => {
|
|
392
|
+
const result = filterByLevel(items);
|
|
393
|
+
expect(result).toHaveLength(4);
|
|
394
|
+
});
|
|
395
|
+
it('unknown level throws an error', () => {
|
|
396
|
+
expect(() => filterByLevel(items, 'unknown')).toThrow('Unknown level "unknown"');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
//# sourceMappingURL=portable-knowledge.test.js.map
|