@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.
@@ -0,0 +1,8 @@
1
+ import {
2
+ ingestAll,
3
+ ingestSynergies
4
+ } from "./chunk-HZEQG3WT.js";
5
+ export {
6
+ ingestAll,
7
+ ingestSynergies
8
+ };
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ browseChanges,
4
+ compareVersions,
3
5
  entityFrequency,
6
+ getChangesSince,
7
+ getSynergy,
4
8
  hybridSearch,
5
9
  listProducts,
10
+ listSynergies,
6
11
  lookupEntity,
7
12
  openDb,
8
13
  recentReleases,
9
14
  searchChanges
10
- } from "./chunk-YFGUTT22.js";
15
+ } from "./chunk-H3466JDH.js";
11
16
 
12
17
  // src/mcp-server.ts
13
18
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -63,6 +68,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
63
68
  mode: { type: "string", enum: ["hybrid", "fts"], description: "hybrid = FTS5+vec via RRF (best for concepts); fts = BM25 only (best for exact terms)", default: "hybrid" },
64
69
  product: { type: "string", description: "Limit to one product (e.g. claude-code, claude-agent-sdk-python, anthropic-cli)" },
65
70
  since: { type: "string", description: "YYYY-MM-DD lower bound on release date" },
71
+ until: { type: "string", description: "YYYY-MM-DD upper bound on release date" },
66
72
  kind: { type: "string", description: "Filter by change kind: added | fixed | breaking | deprecated | renamed | removed | improved | changed" },
67
73
  rerank: { type: "string", enum: ["none", "ollama-judge"], description: "Rerank top-K candidates (hybrid mode only). Defaults to none for speed.", default: "none" },
68
74
  limit: { type: "number", description: "Max results (default 10)", default: 10 }
@@ -93,6 +99,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
93
99
  type: "object",
94
100
  properties: {
95
101
  product: { type: "string", description: "Limit to one product" },
102
+ since: { type: "string", description: "YYYY-MM-DD lower bound on release date" },
96
103
  limit: { type: "number", description: "Max releases", default: 20 }
97
104
  }
98
105
  }
@@ -132,7 +139,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
132
139
  {
133
140
  name: "list_synergies",
134
141
  description: "List curated cross-product workflows (Claude Design \u2194 Code bundle, MCP server portability, etc). Each synergy describes a composition pattern with evidence.",
135
- inputSchema: { type: "object", properties: {} }
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ product: { type: "string", description: "Filter to synergies that mention this product (e.g. claude-code)" }
146
+ }
147
+ }
136
148
  },
137
149
  {
138
150
  name: "read_synergy",
@@ -142,6 +154,47 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
142
154
  properties: { name: { type: "string", description: "Synergy name from list_synergies (e.g. skill-portability)" } },
143
155
  required: ["name"]
144
156
  }
157
+ },
158
+ {
159
+ name: "get_changes_since",
160
+ description: 'Get all changes in a date window, grouped by product+version. The LLM-orientation tool: "what shipped in the last 7 days" without needing a search term. Use this before recommending features to know the current ground truth.',
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ since: { type: "string", description: "Lower bound \u2014 YYYY-MM-DD, full ISO, or relative (e.g. 7d, 2w, 1m, 1y)" },
165
+ until: { type: "string", description: "Upper bound \u2014 YYYY-MM-DD, full ISO, or relative; defaults to now" },
166
+ product: { type: "string", description: "Limit to one product" },
167
+ kind: { type: "string", description: "Filter by change kind: added | fixed | breaking | deprecated | renamed | removed | improved | changed" },
168
+ limit: { type: "number", description: "Max change rows returned (default 200)", default: 200 }
169
+ },
170
+ required: ["since"]
171
+ }
172
+ },
173
+ {
174
+ name: "search_breaking_changes",
175
+ description: "Browse breaking changes in a date window. No search term required \u2014 returns all changes with kind=breaking, most recent first. Use this for upgrade-planning and migration workflows.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ product: { type: "string", description: "Limit to one product" },
180
+ since: { type: "string", description: "Lower bound \u2014 YYYY-MM-DD, full ISO, or relative (e.g. 7d)" },
181
+ until: { type: "string", description: "Upper bound \u2014 YYYY-MM-DD, full ISO, or relative; defaults to now" },
182
+ limit: { type: "number", description: "Max results (default 50)", default: 50 }
183
+ }
184
+ }
185
+ },
186
+ {
187
+ name: "compare_versions",
188
+ description: 'Cumulative diff between two versions of a product, grouped by intermediate release. Use for upgrade planning ("I am on python 0.88.0, what changed through 0.94.0?"). Single call replaces N+1 get_release lookups.',
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ product: { type: "string", description: "Product slug (e.g. anthropic-sdk-python)" },
193
+ from_version: { type: "string", description: "Starting version, exclusive (you are already on this)" },
194
+ to_version: { type: "string", description: "Target version, inclusive" }
195
+ },
196
+ required: ["product", "from_version", "to_version"]
197
+ }
145
198
  }
146
199
  ]
147
200
  }));
@@ -229,6 +282,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
229
282
  mode: optEnum(r, "mode", SEARCH_MODES, "search"),
230
283
  product: optString(r, "product", "search"),
231
284
  since: optString(r, "since", "search"),
285
+ until: optString(r, "until", "search"),
232
286
  kind: optString(r, "kind", "search"),
233
287
  rerank: optEnum(r, "rerank", RERANK_MODES, "search"),
234
288
  limit: optInt(r, "limit", "search")
@@ -266,6 +320,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
266
320
  type: "text",
267
321
  text: handleLatestReleases({
268
322
  product: optString(r, "product", "latest_releases"),
323
+ since: optString(r, "since", "latest_releases"),
269
324
  limit: optInt(r, "limit", "latest_releases")
270
325
  })
271
326
  }
@@ -306,8 +361,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
306
361
  ]
307
362
  };
308
363
  }
309
- case "list_synergies":
310
- return { content: [{ type: "text", text: handleListSynergies() }] };
364
+ case "list_synergies": {
365
+ const r = asRecord(args ?? {}, "list_synergies");
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text",
370
+ text: handleListSynergies({ product: optString(r, "product", "list_synergies") })
371
+ }
372
+ ]
373
+ };
374
+ }
311
375
  case "read_synergy": {
312
376
  const r = asRecord(args, "read_synergy");
313
377
  const synName = requireString(r, "name", "read_synergy");
@@ -319,6 +383,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
319
383
  }
320
384
  return { content: [{ type: "text", text: handleReadSynergy({ name: synName }) }] };
321
385
  }
386
+ case "get_changes_since": {
387
+ const r = asRecord(args, "get_changes_since");
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text",
392
+ text: handleGetChangesSince({
393
+ since: requireString(r, "since", "get_changes_since"),
394
+ until: optString(r, "until", "get_changes_since"),
395
+ product: optString(r, "product", "get_changes_since"),
396
+ kind: optString(r, "kind", "get_changes_since"),
397
+ limit: optInt(r, "limit", "get_changes_since")
398
+ })
399
+ }
400
+ ]
401
+ };
402
+ }
403
+ case "search_breaking_changes": {
404
+ const r = asRecord(args ?? {}, "search_breaking_changes");
405
+ return {
406
+ content: [
407
+ {
408
+ type: "text",
409
+ text: handleSearchBreakingChanges({
410
+ product: optString(r, "product", "search_breaking_changes"),
411
+ since: optString(r, "since", "search_breaking_changes"),
412
+ until: optString(r, "until", "search_breaking_changes"),
413
+ limit: optInt(r, "limit", "search_breaking_changes")
414
+ })
415
+ }
416
+ ]
417
+ };
418
+ }
419
+ case "compare_versions": {
420
+ const r = asRecord(args, "compare_versions");
421
+ return {
422
+ content: [
423
+ {
424
+ type: "text",
425
+ text: handleCompareVersions({
426
+ product: requireString(r, "product", "compare_versions"),
427
+ from_version: requireString(r, "from_version", "compare_versions"),
428
+ to_version: requireString(r, "to_version", "compare_versions")
429
+ })
430
+ }
431
+ ]
432
+ };
433
+ }
322
434
  default:
323
435
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
324
436
  }
@@ -334,6 +446,7 @@ async function handleSearch(args) {
334
446
  const results2 = searchChanges(db, args.query, {
335
447
  product: args.product,
336
448
  since: args.since,
449
+ until: args.until,
337
450
  kind: args.kind,
338
451
  limit
339
452
  });
@@ -355,6 +468,7 @@ async function handleSearch(args) {
355
468
  const results = await hybridSearch(db, args.query, {
356
469
  product: args.product,
357
470
  since: args.since,
471
+ until: args.until,
358
472
  kind: args.kind,
359
473
  rerankProviderName: args.rerank ?? "none",
360
474
  limit
@@ -396,7 +510,7 @@ function handleLookupEntity(args) {
396
510
  }
397
511
  function handleLatestReleases(args) {
398
512
  const limit = args.limit ?? 20;
399
- const releases = recentReleases(db, args.product, limit);
513
+ const releases = recentReleases(db, args.product, limit, args.since);
400
514
  if (releases.length === 0) return "(no releases)";
401
515
  return releases.map((r) => `${r.released_at} ${r.product}@${r.version} (${r.change_count} change${r.change_count === 1 ? "" : "s"})`).join("\n");
402
516
  }
@@ -436,52 +550,173 @@ function handleTopEntities(args) {
436
550
  if (results.length === 0) return `(no entities of type ${args.type})`;
437
551
  return results.map((r) => `${String(r.count).padStart(4)} ${r.first_seen ?? "????-??-??"} ${r.value}`).join("\n");
438
552
  }
439
- function handleListSynergies() {
553
+ function dbSynergiesEmpty() {
554
+ try {
555
+ const row = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='synergies'`).get();
556
+ if (!row) return true;
557
+ const count = db.prepare(`SELECT COUNT(*) AS n FROM synergies`).get();
558
+ return !count || count.n === 0;
559
+ } catch {
560
+ return true;
561
+ }
562
+ }
563
+ function handleListSynergies(args = {}) {
564
+ if (!dbSynergiesEmpty()) {
565
+ try {
566
+ const rows = listSynergies(db, args.product ? { product: args.product } : void 0);
567
+ if (rows.length === 0) {
568
+ return args.product ? `(no synergies mention product "${args.product}")` : "(no synergies)";
569
+ }
570
+ const lines = [`Synergies (${rows.length}):
571
+ `];
572
+ for (const s of rows) {
573
+ const name = s.name ?? "";
574
+ const title = s.title ?? name;
575
+ const products = Array.isArray(s.products) ? s.products.join(", ") : s.products ?? "unknown";
576
+ const trigger = s.trigger ?? "";
577
+ lines.push(`- ${name}: ${title}`);
578
+ lines.push(` products: ${products}`);
579
+ if (trigger) lines.push(` trigger: ${trigger}`);
580
+ lines.push("");
581
+ }
582
+ return lines.join("\n");
583
+ } catch {
584
+ }
585
+ }
586
+ return listSynergiesFromDisk(args.product);
587
+ }
588
+ function listSynergiesFromDisk(productFilter) {
440
589
  if (!existsSync(SYNERGIES_DIR)) return "(synergies dir not found)";
441
590
  const files = readdirSync(SYNERGIES_DIR).filter((f) => f.endsWith(".md") && f !== "INDEX.md");
442
591
  if (files.length === 0) return "(no synergies)";
443
- const lines = [`Synergies (${files.length}):
444
- `];
592
+ const lines = [];
593
+ let count = 0;
445
594
  for (const f of files) {
446
595
  try {
447
596
  const raw = readFileSync(join(SYNERGIES_DIR, f), "utf-8");
448
597
  const { data } = matter(raw);
598
+ const products = Array.isArray(data.products) ? data.products : [];
599
+ if (productFilter && !products.includes(productFilter)) continue;
449
600
  const name = data.name ?? f.replace(/\.md$/, "");
450
601
  const title = data.title ?? name;
451
- const products = Array.isArray(data.products) ? data.products.join(", ") : "unknown";
452
602
  const trigger = data.trigger ?? "";
453
603
  lines.push(`- ${name}: ${title}`);
454
- lines.push(` products: ${products}`);
604
+ lines.push(` products: ${products.length > 0 ? products.join(", ") : "unknown"}`);
455
605
  if (trigger) lines.push(` trigger: ${trigger}`);
456
606
  lines.push("");
607
+ count += 1;
457
608
  } catch {
458
609
  }
459
610
  }
460
- return lines.join("\n");
611
+ if (count === 0) {
612
+ return productFilter ? `(no synergies mention product "${productFilter}")` : "(no synergies)";
613
+ }
614
+ return [`Synergies (${count}):
615
+ `, ...lines].join("\n");
461
616
  }
462
617
  function handleReadSynergy(args) {
463
- if (!existsSync(SYNERGIES_DIR)) return "(synergies dir not found)";
464
618
  if (!SYNERGY_NAME_RE.test(args.name)) {
465
619
  return `(synergy not found: ${args.name})`;
466
620
  }
621
+ if (!dbSynergiesEmpty()) {
622
+ try {
623
+ const synergy = getSynergy(db, args.name);
624
+ if (synergy && synergy.body) {
625
+ return synergy.body;
626
+ }
627
+ } catch {
628
+ }
629
+ }
630
+ if (!existsSync(SYNERGIES_DIR)) return `(synergy not found: ${args.name})`;
467
631
  const files = readdirSync(SYNERGIES_DIR).filter((f) => f.endsWith(".md"));
468
632
  const targetFile = files.find((f) => basename(f, ".md") === args.name);
469
633
  if (targetFile) {
470
- const raw = readFileSync(join(SYNERGIES_DIR, targetFile), "utf-8");
471
- return raw;
634
+ return readFileSync(join(SYNERGIES_DIR, targetFile), "utf-8");
472
635
  }
473
636
  for (const f of files) {
474
637
  try {
475
638
  const raw = readFileSync(join(SYNERGIES_DIR, f), "utf-8");
476
639
  const { data } = matter(raw);
477
- if (data.name === args.name) {
478
- return raw;
479
- }
640
+ if (data.name === args.name) return raw;
480
641
  } catch {
481
642
  }
482
643
  }
483
644
  return `(synergy not found: ${args.name})`;
484
645
  }
646
+ function formatChangesSinceResults(results) {
647
+ if (results.length === 0) {
648
+ return "(no changes in window \u2014 try widening --since)";
649
+ }
650
+ const lines = [];
651
+ let total = 0;
652
+ for (const rel of results) {
653
+ const date = rel.released_at ?? "????-??-??";
654
+ lines.push(`${date} ${rel.product}@${rel.version} (${rel.changes.length} change${rel.changes.length === 1 ? "" : "s"})`);
655
+ for (const c of rel.changes) {
656
+ lines.push(` - [${c.kind}] ${c.text}`);
657
+ }
658
+ lines.push("");
659
+ total += rel.changes.length;
660
+ }
661
+ lines.push(`${total} change${total === 1 ? "" : "s"} across ${results.length} release${results.length === 1 ? "" : "s"}`);
662
+ return lines.join("\n");
663
+ }
664
+ function handleGetChangesSince(args) {
665
+ let results;
666
+ try {
667
+ results = getChangesSince(db, {
668
+ since: args.since,
669
+ until: args.until,
670
+ product: args.product,
671
+ kind: args.kind,
672
+ limit: args.limit ?? 200
673
+ });
674
+ } catch (e) {
675
+ throw new McpError(ErrorCode.InvalidParams, `get_changes_since: ${e.message}`);
676
+ }
677
+ return formatChangesSinceResults(results);
678
+ }
679
+ function handleSearchBreakingChanges(args) {
680
+ let results;
681
+ try {
682
+ results = browseChanges(db, {
683
+ product: args.product,
684
+ since: args.since,
685
+ until: args.until,
686
+ kind: "breaking",
687
+ limit: args.limit ?? 50
688
+ });
689
+ } catch (e) {
690
+ throw new McpError(ErrorCode.InvalidParams, `search_breaking_changes: ${e.message}`);
691
+ }
692
+ if (results.length === 0) {
693
+ return "(no breaking changes \u2014 note: ingest may not have classified any changes as breaking yet)";
694
+ }
695
+ const lines = [];
696
+ for (const r of results) {
697
+ lines.push(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]`);
698
+ lines.push(` ${r.text}`);
699
+ lines.push("");
700
+ }
701
+ lines.push(`${results.length} breaking change${results.length === 1 ? "" : "s"}`);
702
+ return lines.join("\n");
703
+ }
704
+ function handleCompareVersions(args) {
705
+ let results;
706
+ try {
707
+ results = compareVersions(db, {
708
+ product: args.product,
709
+ fromVersion: args.from_version,
710
+ toVersion: args.to_version
711
+ });
712
+ } catch (e) {
713
+ throw new McpError(ErrorCode.InvalidParams, `compare_versions: ${e.message}`);
714
+ }
715
+ if (results.length === 0) {
716
+ return `(no intermediate releases between ${args.product}@${args.from_version} and ${args.product}@${args.to_version})`;
717
+ }
718
+ return formatChangesSinceResults(results);
719
+ }
485
720
  function shutdownMcp() {
486
721
  try {
487
722
  db.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcptoolshop/claude-synergy",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Local mirror of Anthropic product changelogs + curated cross-product synergies. So the LLM agent inside the harness knows what the harness can do.",
5
5
  "type": "module",
6
6
  "exports": {
package/products.yaml CHANGED
@@ -62,14 +62,20 @@
62
62
 
63
63
  products:
64
64
 
65
- # ── Tier 2 / Anthropic CHANGELOG.md ───────────────────────────────────────
65
+ # ── Tier 1 / Anthropic flagship product ──────────────────────────────────
66
+ # FE-1 (2026-05-22): wired up automated sync via GH Releases. The repo
67
+ # publishes a per-version GitHub Release whose body mirrors CHANGELOG.md,
68
+ # so gh-releases yields strictly richer data than parsing the raw file
69
+ # (released_at, html_url, tag_name are all structured). Bumped to tier 1
70
+ # from tier 2 to reflect that this is now a Structured-API source.
66
71
  - name: claude-code
67
72
  display_name: Claude Code
68
- tier: 2
69
- source_url: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
70
- fetch_strategy: git-changelog
71
- # No fetch: block — corpus seeded by initial study-swarm; CHANGELOG.md
72
- # incremental sync deferred to a future phase.
73
+ tier: 1
74
+ source_url: https://github.com/anthropics/claude-code
75
+ fetch_strategy: gh-releases
76
+ fetch:
77
+ type: gh-releases
78
+ repo: anthropics/claude-code
73
79
 
74
80
  # ── Tier 3 / HTML feeds ──────────────────────────────────────────────────
75
81
  - name: claude-api
package/schema-vec.sql CHANGED
@@ -2,7 +2,12 @@
2
2
  -- Run after schema.sql. Tables here are independent of Tier 2a's changes_fts,
3
3
  -- so naive FTS still works on the raw bullets while hybrid uses the contextualized layer.
4
4
  --
5
- -- Dimension: 768 (nomic-embed-text native; Voyage 3 truncatable via Matryoshka)
5
+ -- Dimension: configurable, stored in `schema_meta` under the key
6
+ -- `embedding_dim` (see src/db.ts). The `chunks_vec` virtual table is created
7
+ -- in code by initVecSchema() in src/embed.ts using the negotiated dim so that
8
+ -- switching providers (Ollama 768d, Voyage 768d-truncated, OpenAI 1536d, etc.)
9
+ -- works without manual schema edits. Default is 768 (nomic-embed-text native;
10
+ -- Voyage 3 truncatable via Matryoshka).
6
11
  -- One row per change. Reranking deferred.
7
12
 
8
13
  CREATE TABLE IF NOT EXISTS chunks (
@@ -22,10 +27,9 @@ CREATE TABLE IF NOT EXISTS chunks (
22
27
  CREATE INDEX IF NOT EXISTS idx_chunks_product ON chunks(product, released_at);
23
28
  CREATE INDEX IF NOT EXISTS idx_chunks_change ON chunks(change_id);
24
29
 
25
- -- Vector storage (sqlite-vec extension required)
26
- CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
27
- embedding FLOAT[768]
28
- );
30
+ -- NOTE: `chunks_vec` is created dynamically by src/embed.ts initVecSchema()
31
+ -- so its dimension can match the configured embedding provider. The legacy
32
+ -- 768-dim definition is preserved in db migration code for backward compat.
29
33
 
30
34
  -- FTS5 over the contextualized text (separate from changes_fts which indexes raw bullets)
31
35
  CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(