@mcptoolshop/claude-synergy 1.0.0 → 1.1.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.
@@ -162,6 +162,11 @@ function buildFetchTarget(name, fetch) {
162
162
  case "raw-changelog":
163
163
  if (!fetch.url) throw new Error(`[claude-synergy] ${name}: raw-changelog requires 'url'`);
164
164
  if (!fetch.parser) throw new Error(`[claude-synergy] ${name}: raw-changelog requires 'parser'`);
165
+ if (fetch.parser !== "aider-history" && fetch.parser !== "keep-a-changelog") {
166
+ throw new Error(
167
+ `[claude-synergy] ${name}: raw-changelog 'parser' must be one of: aider-history, keep-a-changelog (got '${fetch.parser}')`
168
+ );
169
+ }
165
170
  target.rawChangelogUrl = fetch.url;
166
171
  target.rawChangelogParser = fetch.parser;
167
172
  break;
@@ -462,8 +467,283 @@ function classifyKind(text) {
462
467
  if (lower.startsWith("improved")) return "improved";
463
468
  return "changed";
464
469
  }
470
+ var URL_INLINE_RE = /\[(?:[^\]]*)\]\((https?:\/\/[^)\s]+)\)|\((https?:\/\/[^)\s]+)\)|(https?:\/\/[^\s)\]]+)/;
471
+ function extractBodySection(body, sectionLabel) {
472
+ const lines = body.split("\n");
473
+ const headingRe = new RegExp(
474
+ `^\\s*(?:\\*\\*${sectionLabel}:?\\*\\*:?\\s*|##+\\s+${sectionLabel}:?\\s*)$`,
475
+ "i"
476
+ );
477
+ const nextSectionRe = /^\s*(?:\*\*[A-Z][A-Za-z][A-Za-z /-]*:?\*\*|##+\s+[A-Z])/;
478
+ let inSection = false;
479
+ const bullets = [];
480
+ let current = "";
481
+ function flush() {
482
+ if (current.trim()) bullets.push(current.trim());
483
+ current = "";
484
+ }
485
+ for (const line of lines) {
486
+ if (!inSection) {
487
+ if (headingRe.test(line)) inSection = true;
488
+ continue;
489
+ }
490
+ if (line.trim() && nextSectionRe.test(line) && !headingRe.test(line)) {
491
+ flush();
492
+ break;
493
+ }
494
+ const numbered = line.match(/^\s*\d+[.)]\s+(.+)$/);
495
+ if (numbered) {
496
+ flush();
497
+ current = numbered[1];
498
+ continue;
499
+ }
500
+ const bulleted = line.match(/^\s*[-*]\s+(.+)$/);
501
+ if (bulleted) {
502
+ flush();
503
+ current = bulleted[1];
504
+ continue;
505
+ }
506
+ if (/^\s{2,}\S/.test(line) && current) {
507
+ current += " " + line.trim();
508
+ continue;
509
+ }
510
+ if (line.trim() === "") {
511
+ flush();
512
+ continue;
513
+ }
514
+ flush();
515
+ }
516
+ flush();
517
+ return bullets;
518
+ }
519
+ function classifyEvidenceSource(url, surroundingText) {
520
+ const lower = `${url} ${surroundingText}`.toLowerCase();
521
+ if (lower.includes("release-notes") || lower.includes("/releases/")) return "release-notes";
522
+ if (lower.includes("changelog")) return "changelog";
523
+ if (lower.includes("blog")) return "blog";
524
+ if (lower.includes("github.com") && lower.includes("readme")) return "github-readme";
525
+ if (lower.includes("/docs/") || lower.includes("docs.")) return "docs";
526
+ if (lower.includes("github.com")) return "github-readme";
527
+ return null;
528
+ }
529
+ function toIsoDate(value) {
530
+ if (!value) return null;
531
+ if (value instanceof Date) {
532
+ if (Number.isNaN(value.getTime())) return null;
533
+ return value.toISOString().slice(0, 10);
534
+ }
535
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
536
+ const d = new Date(value);
537
+ if (Number.isNaN(d.getTime())) return null;
538
+ return d.toISOString().slice(0, 10);
539
+ }
540
+ function ingestSynergies(db, synergyDir) {
541
+ const stats = {
542
+ ingested: 0,
543
+ skipped: 0,
544
+ synergiesAdded: 0,
545
+ productsLinked: 0,
546
+ stepsAdded: 0,
547
+ evidenceAdded: 0,
548
+ changeRefsAdded: 0,
549
+ errors: []
550
+ };
551
+ if (!existsSync2(synergyDir)) {
552
+ return stats;
553
+ }
554
+ let files;
555
+ try {
556
+ files = readdirSync(synergyDir).filter((f) => f.endsWith(".md") && f !== "INDEX.md");
557
+ } catch (e) {
558
+ stats.errors.push({ file: synergyDir, error: e?.message ?? String(e) });
559
+ return stats;
560
+ }
561
+ const upsertSynergy = db.prepare(`
562
+ INSERT INTO synergies (name, title, trigger, status, last_validated, notes_path)
563
+ VALUES (@name, @title, @trigger, @status, @last_validated, @notes_path)
564
+ ON CONFLICT(name) DO UPDATE SET
565
+ title = excluded.title,
566
+ trigger = excluded.trigger,
567
+ status = excluded.status,
568
+ last_validated = excluded.last_validated,
569
+ notes_path = excluded.notes_path
570
+ `);
571
+ const selectSynergyIdByName = db.prepare(
572
+ `SELECT id FROM synergies WHERE name = ?`
573
+ );
574
+ const deleteProductsBySynergy = db.prepare(
575
+ `DELETE FROM synergy_products WHERE synergy_id = ?`
576
+ );
577
+ const deleteStepsBySynergy = db.prepare(
578
+ `DELETE FROM synergy_steps WHERE synergy_id = ?`
579
+ );
580
+ const deleteEvidenceBySynergy = db.prepare(
581
+ `DELETE FROM synergy_evidence WHERE synergy_id = ?`
582
+ );
583
+ const deleteChangeRefsBySynergy = db.prepare(
584
+ `DELETE FROM synergy_change_refs WHERE synergy_id = ?`
585
+ );
586
+ const insertProductLink = db.prepare(`
587
+ INSERT OR IGNORE INTO synergy_products (synergy_id, product) VALUES (?, ?)
588
+ `);
589
+ const insertStep = db.prepare(`
590
+ INSERT INTO synergy_steps (synergy_id, ordinal, text) VALUES (?, ?, ?)
591
+ `);
592
+ const insertEvidence = db.prepare(`
593
+ INSERT INTO synergy_evidence (synergy_id, source_url, quote, source_kind) VALUES (?, ?, ?, ?)
594
+ `);
595
+ const insertChangeRef = db.prepare(`
596
+ INSERT OR IGNORE INTO synergy_change_refs (synergy_id, change_id) VALUES (?, ?)
597
+ `);
598
+ const selectChangeByPvOrdinal = db.prepare(`
599
+ SELECT id FROM changes WHERE product = ? AND version = ? AND ordinal = ?
600
+ `);
601
+ const selectChangeByPv = db.prepare(`
602
+ SELECT id FROM changes WHERE product = ? AND version = ? ORDER BY ordinal
603
+ `);
604
+ const selectProductExists = db.prepare(
605
+ `SELECT name FROM products WHERE name = ?`
606
+ );
607
+ const insertStubProduct = db.prepare(`
608
+ INSERT OR IGNORE INTO products (name, display_name, source_tier, source_url, fetch_strategy, notes)
609
+ VALUES (@name, @display_name, 0, '', 'manual', 'synergy-stub')
610
+ `);
611
+ const tx = db.transaction(() => {
612
+ for (const file of files) {
613
+ try {
614
+ const path = join2(synergyDir, file);
615
+ const raw = readFileSync2(path, "utf-8");
616
+ const parsed = matter(raw);
617
+ const fm = parsed.data;
618
+ const body = parsed.content;
619
+ const slug = (fm.name && typeof fm.name === "string" ? fm.name : basename(file, ".md")).trim();
620
+ if (!slug) {
621
+ stats.errors.push({ file, error: "missing synergy name (frontmatter or filename)" });
622
+ stats.skipped++;
623
+ continue;
624
+ }
625
+ const title = (fm.title && typeof fm.title === "string" ? fm.title : slug).trim();
626
+ const trigger = typeof fm.trigger === "string" ? fm.trigger.trim() : "";
627
+ const status = typeof fm.status === "string" && fm.status.trim() ? fm.status.trim() : "speculative";
628
+ const last_validated = toIsoDate(fm.last_validated) ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
629
+ const notes_path = `synergies/${file}`;
630
+ upsertSynergy.run({
631
+ name: slug,
632
+ title,
633
+ trigger,
634
+ status,
635
+ last_validated,
636
+ notes_path
637
+ });
638
+ const synergyRow = selectSynergyIdByName.get(slug);
639
+ if (!synergyRow) {
640
+ stats.errors.push({ file, error: "failed to resolve synergy id after upsert" });
641
+ stats.skipped++;
642
+ continue;
643
+ }
644
+ const synergyId = synergyRow.id;
645
+ stats.synergiesAdded++;
646
+ stats.ingested++;
647
+ deleteProductsBySynergy.run(synergyId);
648
+ deleteStepsBySynergy.run(synergyId);
649
+ deleteEvidenceBySynergy.run(synergyId);
650
+ deleteChangeRefsBySynergy.run(synergyId);
651
+ const products = Array.isArray(fm.products) ? fm.products.filter((p) => typeof p === "string") : [];
652
+ for (const product of products) {
653
+ const productSlug = product.trim();
654
+ if (!productSlug) continue;
655
+ if (!selectProductExists.get(productSlug)) {
656
+ insertStubProduct.run({ name: productSlug, display_name: productSlug });
657
+ }
658
+ insertProductLink.run(synergyId, productSlug);
659
+ stats.productsLinked++;
660
+ }
661
+ const steps = Array.isArray(fm.steps) && fm.steps.length > 0 ? fm.steps.filter((s) => typeof s === "string").map((s) => s.trim()).filter(Boolean) : extractBodySection(body, "Workflow");
662
+ steps.forEach((stepText, idx) => {
663
+ insertStep.run(synergyId, idx + 1, stepText);
664
+ stats.stepsAdded++;
665
+ });
666
+ if (Array.isArray(fm.evidence) && fm.evidence.length > 0) {
667
+ for (const ev of fm.evidence) {
668
+ if (typeof ev === "string") {
669
+ const m = ev.match(URL_INLINE_RE);
670
+ const url = m ? m[1] ?? m[2] ?? m[3] : null;
671
+ if (!url) continue;
672
+ insertEvidence.run(synergyId, url, ev, classifyEvidenceSource(url, ev));
673
+ stats.evidenceAdded++;
674
+ } else if (ev && typeof ev === "object" && typeof ev.source_url === "string") {
675
+ insertEvidence.run(
676
+ synergyId,
677
+ ev.source_url,
678
+ ev.quote ?? null,
679
+ ev.source_kind ?? classifyEvidenceSource(ev.source_url, ev.quote ?? "")
680
+ );
681
+ stats.evidenceAdded++;
682
+ }
683
+ }
684
+ } else {
685
+ const evidenceBullets = extractBodySection(body, "Evidence");
686
+ for (const bullet of evidenceBullets) {
687
+ const m = bullet.match(URL_INLINE_RE);
688
+ const url = m ? m[1] ?? m[2] ?? m[3] : null;
689
+ if (!url) continue;
690
+ insertEvidence.run(synergyId, url, bullet, classifyEvidenceSource(url, bullet));
691
+ stats.evidenceAdded++;
692
+ }
693
+ }
694
+ if (Array.isArray(fm.change_refs)) {
695
+ for (const ref of fm.change_refs) {
696
+ if (typeof ref === "string") {
697
+ const m = ref.match(/^([^@]+)@([^#]+)(?:#(\d+))?$/);
698
+ if (!m) continue;
699
+ const refProduct = m[1].trim();
700
+ const refVersion = m[2].trim();
701
+ const ordinal = m[3] ? parseInt(m[3], 10) : null;
702
+ if (ordinal !== null) {
703
+ const row = selectChangeByPvOrdinal.get(refProduct, refVersion, ordinal);
704
+ if (row) {
705
+ insertChangeRef.run(synergyId, row.id);
706
+ stats.changeRefsAdded++;
707
+ }
708
+ } else {
709
+ const rows = selectChangeByPv.all(refProduct, refVersion);
710
+ for (const r of rows) {
711
+ insertChangeRef.run(synergyId, r.id);
712
+ stats.changeRefsAdded++;
713
+ }
714
+ }
715
+ } else if (ref && typeof ref === "object") {
716
+ const refProduct = String(ref.product ?? "").trim();
717
+ const refVersion = String(ref.version ?? "").trim();
718
+ if (!refProduct || !refVersion) continue;
719
+ if (typeof ref.ordinal === "number") {
720
+ const row = selectChangeByPvOrdinal.get(refProduct, refVersion, ref.ordinal);
721
+ if (row) {
722
+ insertChangeRef.run(synergyId, row.id);
723
+ stats.changeRefsAdded++;
724
+ }
725
+ } else {
726
+ const rows = selectChangeByPv.all(refProduct, refVersion);
727
+ for (const r of rows) {
728
+ insertChangeRef.run(synergyId, r.id);
729
+ stats.changeRefsAdded++;
730
+ }
731
+ }
732
+ }
733
+ }
734
+ }
735
+ } catch (e) {
736
+ stats.errors.push({ file, error: e?.message ?? String(e) });
737
+ stats.skipped++;
738
+ }
739
+ }
740
+ });
741
+ tx();
742
+ return stats;
743
+ }
465
744
 
466
745
  export {
467
746
  loadProductsConfig,
468
- ingestAll
747
+ ingestAll,
748
+ ingestSynergies
469
749
  };