@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.
@@ -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,CAoDnF"}
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 { parseCursorRuleFile } from "../parse/parse-cursor-rule.js";
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 = parseCursorRuleFile(raw, { origin, idPrefix });
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":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwDD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,GAC1B,cAAc,CAqBhB"}
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
- function parseFrontmatter(raw) {
4
- // Normalize CRLF/CR first: otherwise a trailing `\r` breaks the `\n---`
5
- // separator offset math AND the `key: (.*)$` regex (`$` won't span `\r`),
6
- // dropping description/globs/tags on a Windows-authored .mdc file.
7
- const text = raw.replace(/\r\n?/g, '\n');
8
- if (!text.startsWith('---'))
9
- return { fm: {}, body: text };
10
- const end = text.indexOf('\n---', 3);
11
- if (end < 0)
12
- return { fm: {}, body: text };
13
- const block = text.slice(3, end).trim();
14
- const body = text.slice(end + 4).replace(/^\n+/, '');
15
- const fm = {};
16
- for (const line of block.split('\n')) {
17
- const m = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(line);
18
- if (!m)
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
- else if (key === 'alwaysApply') {
32
- fm.alwaysApply = value.toLowerCase() === 'true';
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
- else {
35
- fm[key] = value;
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
- return { fm, body };
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
- export function parseCursorRuleFile(raw, options) {
53
- const { fm, body } = parseFrontmatter(raw);
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
- return {
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.31",
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.31",
48
- "@shrkcrft/knowledge": "^0.1.0-alpha.31"
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"