@shrkcrft/importer 0.1.0-alpha.31 → 0.1.0-alpha.32
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/dist/api/import-cursor-rules.d.ts.map +1 -1
- package/dist/api/import-cursor-rules.js +6 -2
- package/dist/parse/i-cursor-rule-parse.d.ts +15 -0
- package/dist/parse/i-cursor-rule-parse.d.ts.map +1 -0
- package/dist/parse/i-cursor-rule-parse.js +1 -0
- package/dist/parse/parse-cursor-rule.d.ts +18 -0
- package/dist/parse/parse-cursor-rule.d.ts.map +1 -1
- package/dist/parse/parse-cursor-rule.js +118 -35
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-cursor-rules.d.ts","sourceRoot":"","sources":["../../src/api/import-cursor-rules.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,2BAA2B,CAAC;AAI/E,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qDAAqD;IACrD,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AA6BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"import-cursor-rules.d.ts","sourceRoot":"","sources":["../../src/api/import-cursor-rules.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,2BAA2B,CAAC;AAI/E,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qDAAqD;IACrD,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AA6BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAuDnF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import * as nodePath from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { parseCursorRule } from "../parse/parse-cursor-rule.js";
|
|
4
4
|
import { slugify } from "../parse/slugify.js";
|
|
5
5
|
function walkMdcFiles(start) {
|
|
6
6
|
const out = [];
|
|
@@ -56,7 +56,11 @@ export function importCursorRules(options) {
|
|
|
56
56
|
const origin = nodePath.relative(root, file) || nodePath.basename(file);
|
|
57
57
|
const base = nodePath.basename(file).replace(/\.(mdc|md)$/i, '');
|
|
58
58
|
const idPrefix = `${basePrefix}.${slugify(base) || 'rule'}`;
|
|
59
|
-
const entry =
|
|
59
|
+
const { entry, problems } = parseCursorRule(raw, { origin, idPrefix });
|
|
60
|
+
// Frontmatter THE parser could not read as declared is said out loud (round
|
|
61
|
+
// 15 follow-up, F6) — the old line splitter skipped it without a word.
|
|
62
|
+
for (const message of problems)
|
|
63
|
+
warnings.push({ origin, message });
|
|
60
64
|
let id = entry.id;
|
|
61
65
|
let counter = 2;
|
|
62
66
|
while (seenIds.has(id)) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IImportedEntry } from '../model/imported-entry.js';
|
|
2
|
+
/** One `.mdc` rule read by `parseCursorRule`: the entry, and every reason (part of) its frontmatter was not read. */
|
|
3
|
+
export interface ICursorRuleParse {
|
|
4
|
+
readonly entry: IImportedEntry;
|
|
5
|
+
/**
|
|
6
|
+
* Why frontmatter did not reach the entry — a parse error naming the line
|
|
7
|
+
* (in the top-level structure, a line naming no key: the whole frontmatter
|
|
8
|
+
* is ignored; under one key the importer reads: that key alone), a field of
|
|
9
|
+
* the wrong shape (that field is ignored), an unterminated block. A key the
|
|
10
|
+
* importer does not read is never parsed. `[]`: read as declared. The
|
|
11
|
+
* importer surfaces each as a warning; none is silent.
|
|
12
|
+
*/
|
|
13
|
+
readonly problems: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=i-cursor-rule-parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i-cursor-rule-parse.d.ts","sourceRoot":"","sources":["../../src/parse/i-cursor-rule-parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,qHAAqH;AACrH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IImportedEntry } from '../model/imported-entry.js';
|
|
2
|
+
import type { ICursorRuleParse } from './i-cursor-rule-parse.js';
|
|
2
3
|
/**
|
|
3
4
|
* Parse a single .cursor/rules/*.mdc file. MDC format = YAML frontmatter
|
|
4
5
|
* (between `---` markers) + markdown body. We extract:
|
|
@@ -6,10 +7,27 @@ import type { IImportedEntry } from '../model/imported-entry.js';
|
|
|
6
7
|
* - `globs` / `tags` → tags
|
|
7
8
|
* - `priority` (if present) → KnowledgePriority
|
|
8
9
|
* - the body → content
|
|
10
|
+
*
|
|
11
|
+
* The frontmatter is read by THE parser (`splitFrontmatter` +
|
|
12
|
+
* `parseFrontmatter`, @shrkcrft/core — round 15 follow-up, F6) in its `Text`
|
|
13
|
+
* scalar mode: Cursor writes values bare (`globs: *.ts, *.tsx`,
|
|
14
|
+
* `description: Fix #12`), and they are read verbatim, as the old line
|
|
15
|
+
* splitter read them. A comma-separated `globs` / `tags` string is read as
|
|
16
|
+
* THE parser's inline list, so a quoted item or a brace glob (`*.{ts,tsx}`)
|
|
17
|
+
* stays one item. Only the four keys the importer reads are parsed, each on
|
|
18
|
+
* its own; any other key is skipped unparsed (`IParseFrontmatterOptions.keys`).
|
|
9
19
|
*/
|
|
10
20
|
export interface ICursorRuleOptions {
|
|
11
21
|
origin: string;
|
|
12
22
|
idPrefix: string;
|
|
13
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse one `.mdc` rule, with every reason (part of) its frontmatter was not
|
|
26
|
+
* read — the importer surfaces each as a warning (round 15 follow-up, F6: the
|
|
27
|
+
* old line splitter skipped an unreadable line, a block list or a whole BOM /
|
|
28
|
+
* `--- ` file without a word).
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseCursorRule(raw: string, options: ICursorRuleOptions): ICursorRuleParse;
|
|
31
|
+
/** {@link parseCursorRule}'s entry alone (the frontmatter problems are dropped — `importCursorRules` reports them). */
|
|
14
32
|
export declare function parseCursorRuleFile(raw: string, options: ICursorRuleOptions): IImportedEntry;
|
|
15
33
|
//# sourceMappingURL=parse-cursor-rule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-cursor-rule.d.ts","sourceRoot":"","sources":["../../src/parse/parse-cursor-rule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parse-cursor-rule.d.ts","sourceRoot":"","sources":["../../src/parse/parse-cursor-rule.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,gBAAgB,CAsB1F;AAED,uHAAuH;AACvH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAE5F"}
|
|
@@ -1,41 +1,113 @@
|
|
|
1
|
+
import { FrontmatterScalarMode, parseFrontmatter, parseInlineScalar, splitFrontmatter, } from '@shrkcrft/core';
|
|
1
2
|
import { KnowledgePriority, KnowledgeType } from '@shrkcrft/knowledge';
|
|
2
3
|
import { keywordTags, slugify } from "./slugify.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
/** The keys the importer reads as lists — `description` / `priority` are one value each. */
|
|
5
|
+
const MDC_LIST_KEYS = Object.freeze(['globs', 'tags']);
|
|
6
|
+
/**
|
|
7
|
+
* `.mdc` values are text by contract — read verbatim (no YAML typing, no
|
|
8
|
+
* ` # comment` strip). Only `globs` / `tags` read an inline `[…]` as a list
|
|
9
|
+
* (`listKeys`, round 15 closing A1): `description: [WIP]` is the title `[WIP]`,
|
|
10
|
+
* as the old splitter read it (it was ignored as "a list").
|
|
11
|
+
*/
|
|
12
|
+
const MDC_FRONTMATTER = Object.freeze({
|
|
13
|
+
scalars: FrontmatterScalarMode.Text,
|
|
14
|
+
listKeys: MDC_LIST_KEYS,
|
|
15
|
+
});
|
|
16
|
+
/** The frontmatter keys the importer reads; every other key (`alwaysApply`, a tool's `metadata:`) is skipped unparsed. */
|
|
17
|
+
const MDC_KEYS = Object.freeze(['description', 'globs', 'tags', 'priority']);
|
|
18
|
+
function shapeOf(value) {
|
|
19
|
+
if (!Array.isArray(value))
|
|
20
|
+
return 'a map';
|
|
21
|
+
return value.some((v) => v !== null && typeof v === 'object') ? 'a list of maps' : 'a list';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A comma-separated value (`globs: src/**\/*.ts, test/**`) read through THE
|
|
25
|
+
* parser's inline-list grammar: quote-aware and bracket/brace-aware, so
|
|
26
|
+
* `"a, b"` and `*.{ts,tsx}` each stay one item.
|
|
27
|
+
*/
|
|
28
|
+
function listFromText(value) {
|
|
29
|
+
const r = parseInlineScalar(`[${value}]`, 0, MDC_FRONTMATTER);
|
|
30
|
+
if (!r.ok || !Array.isArray(r.value))
|
|
31
|
+
return undefined;
|
|
32
|
+
return r.value.filter((v) => v !== null && String(v).length > 0).map((v) => String(v));
|
|
33
|
+
}
|
|
34
|
+
function readCursorFrontmatter(raw) {
|
|
35
|
+
const split = splitFrontmatter(raw);
|
|
36
|
+
const problems = [];
|
|
37
|
+
if (split.unterminated) {
|
|
38
|
+
problems.push('frontmatter: an opening --- line has no closing --- line — the whole file is read as the body');
|
|
39
|
+
}
|
|
40
|
+
if (split.frontmatter === undefined)
|
|
41
|
+
return { fm: {}, body: split.body, problems };
|
|
42
|
+
const at = { ...MDC_FRONTMATTER, lineOffset: split.lineOffset };
|
|
43
|
+
// The top-level structure alone (no key read): a line that names no key
|
|
44
|
+
// means the block is not frontmatter THE parser can read at all.
|
|
45
|
+
const structure = parseFrontmatter(split.frontmatter, { ...at, keys: [] });
|
|
46
|
+
if (!structure.ok) {
|
|
47
|
+
problems.push(`frontmatter not read (${structure.error.message}) — description, globs, tags and priority are ignored; the title comes from the body`);
|
|
48
|
+
return { fm: {}, body: split.body, problems };
|
|
49
|
+
}
|
|
50
|
+
// Each key the importer reads, parsed on its own — every other block is
|
|
51
|
+
// skipped unparsed (`alwaysApply`, a tool's `metadata:`): YAML the parser
|
|
52
|
+
// does not speak under one key costs that key alone, never its siblings (the
|
|
53
|
+
// old splitter read every other line regardless).
|
|
54
|
+
const fields = {};
|
|
55
|
+
for (const key of MDC_KEYS) {
|
|
56
|
+
const read = parseFrontmatter(split.frontmatter, { ...at, keys: [key] });
|
|
57
|
+
if (!read.ok) {
|
|
58
|
+
problems.push(`${key}: not read (${read.error.message}) — ignored${key === 'description' ? '; the title comes from the body' : ''}`);
|
|
19
59
|
continue;
|
|
20
|
-
const key = m[1];
|
|
21
|
-
let value = m[2].trim();
|
|
22
|
-
// Strip wrapping quotes / brackets.
|
|
23
|
-
if (value.startsWith('"') && value.endsWith('"'))
|
|
24
|
-
value = value.slice(1, -1);
|
|
25
|
-
if (value.startsWith("'") && value.endsWith("'"))
|
|
26
|
-
value = value.slice(1, -1);
|
|
27
|
-
if (key === 'tags' || key === 'globs') {
|
|
28
|
-
const list = value.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
|
|
29
|
-
fm[key] = list.filter(Boolean);
|
|
30
60
|
}
|
|
31
|
-
|
|
32
|
-
|
|
61
|
+
const value = read.value[key];
|
|
62
|
+
if (value !== undefined)
|
|
63
|
+
fields[key] = value;
|
|
64
|
+
}
|
|
65
|
+
const text = (key) => {
|
|
66
|
+
const v = fields[key];
|
|
67
|
+
if (v === undefined || v === null)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (typeof v === 'string')
|
|
70
|
+
return v.trim().length > 0 ? v : undefined;
|
|
71
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
72
|
+
return String(v);
|
|
73
|
+
problems.push(`${key}: must be a single value (got ${shapeOf(v)}) — ignored`);
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
const list = (key) => {
|
|
77
|
+
const v = fields[key];
|
|
78
|
+
if (v === undefined || v === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (typeof v === 'string') {
|
|
81
|
+
const items = listFromText(v);
|
|
82
|
+
if (items === undefined) {
|
|
83
|
+
problems.push(`${key}: "${v}" is not a comma-separated list — ignored`);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return [...items];
|
|
33
87
|
}
|
|
34
|
-
|
|
35
|
-
|
|
88
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
89
|
+
return [String(v)];
|
|
90
|
+
const items = Array.isArray(v) ? v : undefined;
|
|
91
|
+
if (items === undefined || items.some((x) => x !== null && typeof x === 'object')) {
|
|
92
|
+
problems.push(`${key}: must be a list of plain values or a comma-separated string (got ${shapeOf(v)}) — ignored`);
|
|
93
|
+
return undefined;
|
|
36
94
|
}
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
return items.filter((x) => x !== null && String(x).length > 0).map((x) => String(x));
|
|
96
|
+
};
|
|
97
|
+
const fm = {};
|
|
98
|
+
const description = text('description');
|
|
99
|
+
if (description !== undefined)
|
|
100
|
+
fm.description = description;
|
|
101
|
+
const tags = list('tags');
|
|
102
|
+
if (tags !== undefined)
|
|
103
|
+
fm.tags = tags;
|
|
104
|
+
const globs = list('globs');
|
|
105
|
+
if (globs !== undefined)
|
|
106
|
+
fm.globs = globs;
|
|
107
|
+
const priority = text('priority');
|
|
108
|
+
if (priority !== undefined)
|
|
109
|
+
fm.priority = priority;
|
|
110
|
+
return { fm, body: split.body, problems };
|
|
39
111
|
}
|
|
40
112
|
function priorityFromString(input) {
|
|
41
113
|
switch ((input ?? '').toLowerCase()) {
|
|
@@ -49,8 +121,14 @@ function priorityFromString(input) {
|
|
|
49
121
|
return KnowledgePriority.Medium;
|
|
50
122
|
}
|
|
51
123
|
}
|
|
52
|
-
|
|
53
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Parse one `.mdc` rule, with every reason (part of) its frontmatter was not
|
|
126
|
+
* read — the importer surfaces each as a warning (round 15 follow-up, F6: the
|
|
127
|
+
* old line splitter skipped an unreadable line, a block list or a whole BOM /
|
|
128
|
+
* `--- ` file without a word).
|
|
129
|
+
*/
|
|
130
|
+
export function parseCursorRule(raw, options) {
|
|
131
|
+
const { fm, body, problems } = readCursorFrontmatter(raw);
|
|
54
132
|
const title = fm.description ?? body.split('\n').find((l) => l.trim().length > 0) ?? options.idPrefix;
|
|
55
133
|
const baseSlug = slugify(title) || slugify(options.idPrefix);
|
|
56
134
|
const tags = [
|
|
@@ -60,7 +138,7 @@ export function parseCursorRuleFile(raw, options) {
|
|
|
60
138
|
...keywordTags(title),
|
|
61
139
|
]),
|
|
62
140
|
];
|
|
63
|
-
|
|
141
|
+
const entry = {
|
|
64
142
|
id: `${options.idPrefix}.${baseSlug}`,
|
|
65
143
|
title: title.slice(0, 100),
|
|
66
144
|
type: KnowledgeType.Rule,
|
|
@@ -70,4 +148,9 @@ export function parseCursorRuleFile(raw, options) {
|
|
|
70
148
|
content: body.trim(),
|
|
71
149
|
origin: options.origin,
|
|
72
150
|
};
|
|
151
|
+
return { entry, problems };
|
|
152
|
+
}
|
|
153
|
+
/** {@link parseCursorRule}'s entry alone (the frontmatter problems are dropped — `importCursorRules` reports them). */
|
|
154
|
+
export function parseCursorRuleFile(raw, options) {
|
|
155
|
+
return parseCursorRule(raw, options).entry;
|
|
73
156
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/importer",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.32",
|
|
4
4
|
"description": "SharkCraft importer: parse AGENTS.md / CLAUDE.md / .cursor/rules into structured knowledge entries.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
48
|
-
"@shrkcrft/knowledge": "^0.1.0-alpha.
|
|
47
|
+
"@shrkcrft/core": "^0.1.0-alpha.32",
|
|
48
|
+
"@shrkcrft/knowledge": "^0.1.0-alpha.32"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|