@mcptoolshop/claude-synergy 0.0.0 → 1.0.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.
@@ -0,0 +1,469 @@
1
+ // src/ingest.ts
2
+ import { readdirSync, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "fs";
3
+ import { join as join2, relative, basename } from "path";
4
+ import { createHash } from "crypto";
5
+ import matter from "gray-matter";
6
+ import TurndownService from "turndown";
7
+
8
+ // src/extract.ts
9
+ var PATTERNS = [
10
+ // env vars — uppercase with underscores, prefixed by known prefixes
11
+ {
12
+ type: "env_var",
13
+ pattern: /\b((?:CLAUDE_CODE|ANTHROPIC|MCP|GH|GITHUB|AWS|GOOGLE|VOYAGE|OLLAMA|NPM|NODE)_[A-Z][A-Z0-9_]+)\b/g
14
+ },
15
+ // slash commands — lowercase with hyphens, must start with /
16
+ {
17
+ type: "slash_command",
18
+ pattern: /(?<![A-Za-z0-9_/])(\/[a-z][a-z0-9-]+)(?![A-Za-z0-9-])/g
19
+ },
20
+ // CLI options — --long-form or -s shorthand
21
+ {
22
+ type: "cli_option",
23
+ pattern: /(?<![A-Za-z0-9])(--[a-z][a-z0-9-]+)(?![A-Za-z0-9-])/g
24
+ },
25
+ // Beta headers — kebab-case with date suffix YYYY-MM-DD
26
+ {
27
+ type: "beta_header",
28
+ pattern: /\b([a-z][a-z0-9-]*-20\d{2}-\d{2}-\d{2})\b/g
29
+ },
30
+ // Model IDs — claude-{opus,sonnet,haiku}-N(-N)?(-YYYYMMDD)?
31
+ {
32
+ type: "model_id",
33
+ pattern: /\b(claude-(?:opus|sonnet|haiku)-\d+(?:-\d+)?(?:-\d{8})?)\b/g
34
+ },
35
+ // Hook event names — defined set
36
+ {
37
+ type: "hook_event",
38
+ pattern: /\b(PreToolUse|PostToolUse|SessionStart|SessionEnd|SubagentStart|SubagentStop|Stop|UserPromptSubmit|Setup|Notification|PreCompact|ConfigChange|WorktreeCreate)\b/g
39
+ },
40
+ // Setting keys — camelCase under settings.json (heuristic)
41
+ {
42
+ type: "setting_key",
43
+ pattern: /\b((?:permissions|hooks|worktree|mcpServers|spinnerVerbs|allowed_non_write_users)\.[a-zA-Z]+)\b/g
44
+ },
45
+ // CVE identifiers
46
+ {
47
+ type: "cve",
48
+ pattern: /\b(CVE-20\d{2}-\d{4,7})\b/g
49
+ },
50
+ // GHSA identifiers
51
+ {
52
+ type: "ghsa",
53
+ pattern: /\b(GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4})\b/g
54
+ }
55
+ ];
56
+ function extractEntities(text) {
57
+ const found = [];
58
+ const seen = /* @__PURE__ */ new Set();
59
+ for (const { type, pattern } of PATTERNS) {
60
+ pattern.lastIndex = 0;
61
+ let match;
62
+ while ((match = pattern.exec(text)) !== null) {
63
+ const value = match[1];
64
+ const key = `${type}:${value}`;
65
+ if (!seen.has(key)) {
66
+ seen.add(key);
67
+ found.push([type, value]);
68
+ }
69
+ }
70
+ }
71
+ return found;
72
+ }
73
+
74
+ // src/products-config.ts
75
+ import { readFileSync, existsSync } from "fs";
76
+ import { dirname, join } from "path";
77
+ import { fileURLToPath } from "url";
78
+ import { parse as parseYaml } from "yaml";
79
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
80
+ var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
81
+ var GH_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
82
+ var warnedAboutMissingYaml = false;
83
+ function loadProductsConfig(yamlPath) {
84
+ const resolved = yamlPath ?? resolveYamlPath();
85
+ if (!resolved || !existsSync(resolved)) {
86
+ if (!warnedAboutMissingYaml) {
87
+ console.error(`[claude-synergy] products.yaml not found at ${resolved}; using hardcoded defaults`);
88
+ warnedAboutMissingYaml = true;
89
+ }
90
+ return null;
91
+ }
92
+ let raw;
93
+ try {
94
+ raw = readFileSync(resolved, "utf-8");
95
+ } catch (e) {
96
+ console.error(`[claude-synergy] could not read ${resolved}: ${e.message}; using hardcoded defaults`);
97
+ return null;
98
+ }
99
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
100
+ let parsed;
101
+ try {
102
+ parsed = parseYaml(raw);
103
+ } catch (e) {
104
+ throw new Error(`[claude-synergy] products.yaml parse error: ${e.message}`);
105
+ }
106
+ if (!parsed || !Array.isArray(parsed.products)) {
107
+ throw new Error(`[claude-synergy] products.yaml missing required 'products' array`);
108
+ }
109
+ const productMeta = {};
110
+ const fetchTargets = [];
111
+ for (const p of parsed.products) {
112
+ if (!p.name || !p.display_name) {
113
+ throw new Error(`[claude-synergy] products.yaml entry missing name/display_name: ${JSON.stringify(p)}`);
114
+ }
115
+ if (!PRODUCT_NAME_RE.test(p.name)) {
116
+ throw new Error(
117
+ `[claude-synergy] products.yaml: invalid product name '${p.name}' \u2014 must match /^[a-z0-9][a-z0-9-]*$/ (lowercase alphanumeric + hyphens, no leading hyphen)`
118
+ );
119
+ }
120
+ productMeta[p.name] = {
121
+ name: p.name,
122
+ display_name: p.display_name,
123
+ source_tier: p.tier ?? 1,
124
+ source_url: p.source_url ?? "",
125
+ fetch_strategy: p.fetch_strategy ?? "gh-releases",
126
+ notes: p.notes ?? null
127
+ };
128
+ if (p.fetch) {
129
+ const target = buildFetchTarget(p.name, p.fetch);
130
+ if (target) {
131
+ if (target.strategy === "gh-releases" && target.repo && !GH_REPO_RE.test(target.repo)) {
132
+ throw new Error(
133
+ `[claude-synergy] products.yaml: ${p.name}: invalid repo '${target.repo}' \u2014 must match 'owner/repo' (alphanumeric + dot/underscore/hyphen)`
134
+ );
135
+ }
136
+ fetchTargets.push(target);
137
+ }
138
+ }
139
+ }
140
+ return { productMeta, fetchTargets };
141
+ }
142
+ function buildFetchTarget(name, fetch) {
143
+ if (!fetch) return null;
144
+ const target = { product: name, strategy: fetch.type };
145
+ switch (fetch.type) {
146
+ case "gh-releases":
147
+ if (!fetch.repo) throw new Error(`[claude-synergy] ${name}: gh-releases requires 'repo'`);
148
+ target.repo = fetch.repo;
149
+ if (fetch.multi_package) target.multiPackage = true;
150
+ break;
151
+ case "rss":
152
+ if (!fetch.url) throw new Error(`[claude-synergy] ${name}: rss requires 'url'`);
153
+ target.rssUrl = fetch.url;
154
+ if (fetch.title_filter) {
155
+ try {
156
+ target.rssTitleFilter = new RegExp(fetch.title_filter, "i");
157
+ } catch (e) {
158
+ throw new Error(`[claude-synergy] ${name}: invalid rss title_filter regex: ${e.message}`);
159
+ }
160
+ }
161
+ break;
162
+ case "raw-changelog":
163
+ if (!fetch.url) throw new Error(`[claude-synergy] ${name}: raw-changelog requires 'url'`);
164
+ if (!fetch.parser) throw new Error(`[claude-synergy] ${name}: raw-changelog requires 'parser'`);
165
+ target.rawChangelogUrl = fetch.url;
166
+ target.rawChangelogParser = fetch.parser;
167
+ break;
168
+ case "html-scrape":
169
+ if (!fetch.parser) throw new Error(`[claude-synergy] ${name}: html-scrape requires 'parser'`);
170
+ target.htmlParser = fetch.parser;
171
+ break;
172
+ case "catalog":
173
+ if (!fetch.catalog_type) throw new Error(`[claude-synergy] ${name}: catalog requires 'catalog_type'`);
174
+ target.catalogType = fetch.catalog_type;
175
+ if (fetch.max_entries) target.catalogMaxEntries = fetch.max_entries;
176
+ break;
177
+ case "playwright":
178
+ break;
179
+ default:
180
+ throw new Error(`[claude-synergy] ${name}: unknown fetch type ${fetch.type}`);
181
+ }
182
+ return target;
183
+ }
184
+ function resolveYamlPath() {
185
+ const candidates = [
186
+ join(__dirname2, "..", "products.yaml"),
187
+ join(process.cwd(), "products.yaml")
188
+ ];
189
+ for (const p of candidates) {
190
+ try {
191
+ readFileSync(p);
192
+ return p;
193
+ } catch {
194
+ }
195
+ }
196
+ return candidates[0];
197
+ }
198
+
199
+ // src/ingest.ts
200
+ var turndown = new TurndownService({
201
+ headingStyle: "atx",
202
+ bulletListMarker: "-",
203
+ codeBlockStyle: "fenced",
204
+ emDelimiter: "_"
205
+ });
206
+ turndown.addRule("plainLinks", {
207
+ filter: "a",
208
+ replacement: (content, node) => {
209
+ const href = node.getAttribute?.("href");
210
+ return href ? `${content} (${href})` : content;
211
+ }
212
+ });
213
+ function maybeConvertHtmlToMarkdown(content) {
214
+ const tagCount = (content.match(/<\/?[a-z][^>]*>/gi) || []).length;
215
+ if (tagCount < 5) return content;
216
+ try {
217
+ return turndown.turndown(content);
218
+ } catch {
219
+ return content;
220
+ }
221
+ }
222
+ function compactCommitDumpBody(bullets) {
223
+ if (bullets.length < 10) return bullets;
224
+ const SHA_PREFIX = /^\s*[0-9a-f]{7,40}\b/i;
225
+ const commitLines = bullets.filter((b) => SHA_PREFIX.test(b)).length;
226
+ const ratio = commitLines / bullets.length;
227
+ if (commitLines >= 10 && ratio >= 0.5) {
228
+ const nonCommitLines = bullets.length - commitLines;
229
+ const noteParts = [`Auto-generated release notes: ${commitLines} commits from previous release. See source_url for full commit list.`];
230
+ if (nonCommitLines > 0) {
231
+ const preserved = bullets.filter((b) => !SHA_PREFIX.test(b));
232
+ noteParts.push(...preserved);
233
+ }
234
+ return noteParts;
235
+ }
236
+ return bullets;
237
+ }
238
+ function contentHash(raw) {
239
+ return createHash("sha256").update(raw).digest("hex").slice(0, 16);
240
+ }
241
+ var HARDCODED_PRODUCT_META = {
242
+ "claude-code": { tier: 2, strategy: "git-changelog", url: "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md", display: "Claude Code" },
243
+ "claude-api": { tier: 3, strategy: "webfetch", url: "https://platform.claude.com/docs/en/release-notes/overview", display: "Claude API / Platform" },
244
+ "anthropic-apps": { tier: 3, strategy: "webfetch", url: "https://support.claude.com/en/articles/12138966-release-notes", display: "Anthropic Apps (Design / Cowork / Chat / Mobile)" },
245
+ "claude-agent-sdk-python": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/claude-agent-sdk-python", display: "Claude Agent SDK (Python)" },
246
+ "claude-agent-sdk-typescript": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/claude-agent-sdk-typescript", display: "Claude Agent SDK (TypeScript)" },
247
+ "anthropic-cli": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-cli", display: "Anthropic CLI (ant)" },
248
+ "anthropic-sdk-python": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-python", display: "Anthropic SDK (Python)" },
249
+ "anthropic-sdk-typescript": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-typescript", display: "Anthropic SDK (TypeScript)" },
250
+ "anthropic-sdk-go": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-go", display: "Anthropic SDK (Go)" },
251
+ "anthropic-sdk-java": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-java", display: "Anthropic SDK (Java)" },
252
+ "anthropic-sdk-ruby": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-ruby", display: "Anthropic SDK (Ruby)" },
253
+ "anthropic-sdk-csharp": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-csharp", display: "Anthropic SDK (C#)" },
254
+ "anthropic-sdk-php": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/anthropic-sdk-php", display: "Anthropic SDK (PHP)" },
255
+ "claude-code-action": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/claude-code-action", display: "Claude Code Action (GitHub Action)" },
256
+ "claude-code-security-review": { tier: 1, strategy: "gh-releases", url: "https://github.com/anthropics/claude-code-security-review", display: "Claude Code Security Review (GitHub Action)" },
257
+ "skills": { tier: 4, strategy: "git-snapshot", url: "https://github.com/anthropics/skills", display: "Anthropic Skills Catalog" },
258
+ "plugins-official": { tier: 4, strategy: "git-snapshot", url: "https://github.com/anthropics/claude-plugins-official", display: "Claude Plugins (Official)" },
259
+ "plugins-community": { tier: 4, strategy: "git-snapshot", url: "https://github.com/anthropics/claude-plugins-community", display: "Claude Plugins (Community)" },
260
+ "plugins-knowledge-work": { tier: 4, strategy: "git-snapshot", url: "https://github.com/anthropics/knowledge-work-plugins", display: "Knowledge Work Plugins (Cowork)" },
261
+ // Tier 4a additions — MCP ecosystem
262
+ "mcp-python-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/python-sdk", display: "MCP Python SDK" },
263
+ "mcp-typescript-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/typescript-sdk", display: "MCP TypeScript SDK" },
264
+ "mcp-go-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/go-sdk", display: "MCP Go SDK" },
265
+ "mcp-java-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/java-sdk", display: "MCP Java SDK" },
266
+ "mcp-csharp-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/csharp-sdk", display: "MCP C# SDK" },
267
+ "mcp-kotlin-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/kotlin-sdk", display: "MCP Kotlin SDK" },
268
+ "mcp-ruby-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/ruby-sdk", display: "MCP Ruby SDK" },
269
+ "mcp-swift-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/swift-sdk", display: "MCP Swift SDK" },
270
+ "mcp-rust-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/rust-sdk", display: "MCP Rust SDK" },
271
+ "mcp-php-sdk": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/php-sdk", display: "MCP PHP SDK" },
272
+ "mcp-spec": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/modelcontextprotocol", display: "MCP Specification" },
273
+ "mcp-inspector": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/inspector", display: "MCP Inspector" },
274
+ "mcp-registry": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/registry", display: "MCP Registry" },
275
+ "mcp-mcpb": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/mcpb", display: "MCP Bundle (mcpb)" },
276
+ "mcp-conformance": { tier: 1, strategy: "gh-releases", url: "https://github.com/modelcontextprotocol/conformance", display: "MCP Conformance" },
277
+ // Tier 4a additions — non-Anthropic AI dev tools
278
+ "continue-dev": { tier: 1, strategy: "gh-releases", url: "https://github.com/continuedev/continue", display: "Continue.dev" },
279
+ "continue-cli": { tier: 1, strategy: "gh-releases", url: "https://github.com/continuedev/continue-cli", display: "Continue.dev CLI" },
280
+ "cursor": { tier: 3, strategy: "rss", url: "https://cursor.com/changelog/rss.xml", display: "Cursor" },
281
+ "cody-enterprise": { tier: 3, strategy: "rss", url: "https://sourcegraph.com/changelog/featured.rss", display: "Sourcegraph Cody Enterprise" },
282
+ "aider": { tier: 2, strategy: "raw-changelog", url: "https://raw.githubusercontent.com/Aider-AI/aider/main/HISTORY.md", display: "Aider" },
283
+ // Tier 4b additions — HTML-scraped sources
284
+ "github-copilot": { tier: 3, strategy: "html-scrape", url: "https://github.blog/changelog/label/copilot/", display: "GitHub Copilot" },
285
+ "vscode-copilot-chat": { tier: 3, strategy: "html-scrape", url: "https://code.visualstudio.com/updates/", display: "VS Code Copilot Chat (editor)" },
286
+ "windsurf": { tier: 3, strategy: "html-scrape", url: "https://windsurf.com/changelog", display: "Windsurf (Cognition)" }
287
+ };
288
+ var PRODUCT_META = (() => {
289
+ const cfg = loadProductsConfig();
290
+ if (!cfg) return HARDCODED_PRODUCT_META;
291
+ const out = {};
292
+ for (const [name, meta] of Object.entries(cfg.productMeta)) {
293
+ out[name] = {
294
+ tier: meta.source_tier,
295
+ strategy: meta.fetch_strategy,
296
+ url: meta.source_url,
297
+ display: meta.display_name
298
+ };
299
+ }
300
+ return out;
301
+ })();
302
+ function ingestAll(db, productsRoot) {
303
+ const stats = {
304
+ productsCount: 0,
305
+ releasesAdded: 0,
306
+ changesAdded: 0,
307
+ entitiesAdded: 0,
308
+ skipped: 0,
309
+ total: 0,
310
+ errors: []
311
+ };
312
+ const productDirs = readdirSync(productsRoot).filter((name) => {
313
+ try {
314
+ return statSync(join2(productsRoot, name)).isDirectory();
315
+ } catch {
316
+ return false;
317
+ }
318
+ });
319
+ const insertProduct = db.prepare(`
320
+ INSERT OR REPLACE INTO products (name, display_name, source_tier, source_url, fetch_strategy, notes)
321
+ VALUES (@name, @display_name, @source_tier, @source_url, @fetch_strategy, @notes)
322
+ `);
323
+ const insertRelease = db.prepare(`
324
+ INSERT OR REPLACE INTO releases (product, version, released_at, notes_path, fetched_at, source_url, bundle_size_kb, notes_hash)
325
+ VALUES (@product, @version, @released_at, @notes_path, @fetched_at, @source_url, @bundle_size_kb, @notes_hash)
326
+ `);
327
+ const insertChange = db.prepare(`
328
+ INSERT INTO changes (product, version, ordinal, kind, text)
329
+ VALUES (@product, @version, @ordinal, @kind, @text)
330
+ `);
331
+ const deleteChanges = db.prepare(`DELETE FROM changes WHERE product = ? AND version = ?`);
332
+ const selectExistingHash = db.prepare(
333
+ `SELECT notes_hash FROM releases WHERE product = ? AND version = ?`
334
+ );
335
+ const insertEntity = db.prepare(`
336
+ INSERT INTO entities (change_id, entity_type, entity_value)
337
+ VALUES (?, ?, ?)
338
+ `);
339
+ for (const product of productDirs) {
340
+ const releasesDir = join2(productsRoot, product, "releases");
341
+ if (!existsSync2(releasesDir)) {
342
+ continue;
343
+ }
344
+ let releaseFiles;
345
+ try {
346
+ releaseFiles = readdirSync(releasesDir).filter((f) => f.endsWith(".md"));
347
+ } catch {
348
+ continue;
349
+ }
350
+ if (releaseFiles.length === 0) {
351
+ continue;
352
+ }
353
+ stats.productsCount++;
354
+ const meta = PRODUCT_META[product] ?? {
355
+ tier: 1,
356
+ strategy: "gh-releases",
357
+ url: "",
358
+ display: product
359
+ };
360
+ insertProduct.run({
361
+ name: product,
362
+ display_name: meta.display,
363
+ source_tier: meta.tier,
364
+ source_url: meta.url,
365
+ fetch_strategy: meta.strategy,
366
+ notes: null
367
+ });
368
+ const ingestTransaction = db.transaction(() => {
369
+ for (const file of releaseFiles) {
370
+ try {
371
+ stats.total++;
372
+ const path = join2(releasesDir, file);
373
+ const raw = readFileSync2(path, "utf-8");
374
+ const { data: fm, content } = matter(raw);
375
+ const fmTyped = fm;
376
+ const baseVersion = fmTyped.version ?? basename(file, ".md");
377
+ const version = fmTyped.sub_product ? `${baseVersion}-${fmTyped.sub_product}` : baseVersion;
378
+ const hash = contentHash(raw);
379
+ const existingRow = selectExistingHash.get(product, version);
380
+ if (existingRow?.notes_hash === hash) {
381
+ stats.skipped++;
382
+ continue;
383
+ }
384
+ const released_at = fmTyped.released_at ?? null;
385
+ const source_url = fmTyped.source_url ?? "";
386
+ const fetched_at = fmTyped.fetched_at ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
387
+ const bundle_size_kb = fmTyped.bundle_size_kb_delta ?? null;
388
+ deleteChanges.run(product, version);
389
+ insertRelease.run({
390
+ product,
391
+ version,
392
+ released_at,
393
+ notes_path: relative(productsRoot, path).replace(/\\/g, "/"),
394
+ fetched_at,
395
+ source_url,
396
+ bundle_size_kb,
397
+ notes_hash: hash
398
+ });
399
+ stats.releasesAdded++;
400
+ const bullets = compactCommitDumpBody(parseBullets(maybeConvertHtmlToMarkdown(content)));
401
+ bullets.forEach((bullet, i) => {
402
+ const kind = classifyKind(bullet);
403
+ const result = insertChange.run({
404
+ product,
405
+ version,
406
+ ordinal: i + 1,
407
+ kind,
408
+ text: bullet
409
+ });
410
+ stats.changesAdded++;
411
+ const changeId = result.lastInsertRowid;
412
+ const entities = extractEntities(bullet);
413
+ for (const [type, value] of entities) {
414
+ insertEntity.run(changeId, type, value);
415
+ stats.entitiesAdded++;
416
+ }
417
+ });
418
+ } catch (e) {
419
+ stats.errors.push({ file, error: e.message });
420
+ }
421
+ }
422
+ });
423
+ ingestTransaction();
424
+ }
425
+ return stats;
426
+ }
427
+ function parseBullets(content) {
428
+ const lines = content.split("\n");
429
+ const bullets = [];
430
+ let current = "";
431
+ let inFencedBlock = false;
432
+ for (const line of lines) {
433
+ if (line.trim().startsWith("```")) {
434
+ inFencedBlock = !inFencedBlock;
435
+ continue;
436
+ }
437
+ if (inFencedBlock) continue;
438
+ const bulletMatch = line.match(/^[-*] (.+)$/);
439
+ if (bulletMatch) {
440
+ if (current) bullets.push(current.trim());
441
+ current = bulletMatch[1];
442
+ } else if (line.match(/^\s{2,}\S/) && current) {
443
+ current += " " + line.trim();
444
+ } else if (line.trim() === "" || line.match(/^#/) || line.match(/^\|/)) {
445
+ if (current) {
446
+ bullets.push(current.trim());
447
+ current = "";
448
+ }
449
+ }
450
+ }
451
+ if (current) bullets.push(current.trim());
452
+ return bullets;
453
+ }
454
+ function classifyKind(text) {
455
+ const lower = text.toLowerCase();
456
+ if (lower.includes("breaking")) return "breaking";
457
+ if (lower.startsWith("added") || lower.match(/^new /)) return "added";
458
+ if (lower.startsWith("fixed") || lower.startsWith("fix ")) return "fixed";
459
+ if (lower.startsWith("removed") || lower.startsWith("retired")) return "removed";
460
+ if (lower.includes("deprecat")) return "deprecated";
461
+ if (lower.startsWith("renamed")) return "renamed";
462
+ if (lower.startsWith("improved")) return "improved";
463
+ return "changed";
464
+ }
465
+
466
+ export {
467
+ loadProductsConfig,
468
+ ingestAll
469
+ };