@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.
- package/CHANGELOG.md +126 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +21 -0
- package/README.es.md +340 -0
- package/README.fr.md +340 -0
- package/README.hi.md +340 -0
- package/README.it.md +340 -0
- package/README.ja.md +340 -0
- package/README.md +337 -5
- package/README.pt-BR.md +340 -0
- package/README.zh.md +340 -0
- package/dist/chunk-HCIZPSW4.js +469 -0
- package/dist/chunk-YFGUTT22.js +754 -0
- package/dist/cli.js +2090 -0
- package/dist/fetch-playwright-HQ6OTMSQ.js +80 -0
- package/dist/ingest-3LJNQWS7.js +6 -0
- package/dist/mcp-server.js +497 -0
- package/package.json +81 -7
- package/products.yaml +456 -0
- package/schema-vec.sql +43 -0
- package/schema.sql +155 -0
- package/synergies/01-skill-portability.md +33 -0
- package/synergies/02-mcp-server-portability.md +33 -0
- package/synergies/03-design-to-code-bundle.md +33 -0
- package/synergies/04-computer-use-cross-surface.md +33 -0
- package/synergies/05-ollama-cost-shifting.md +36 -0
- package/synergies/06-agent-sdk-graduation.md +34 -0
- package/synergies/07-code-review-in-ci.md +33 -0
- package/synergies/08-universal-skill-md-format.md +46 -0
- package/synergies/09-mcp-across-seven-surfaces.md +48 -0
- package/synergies/10-anthropic-byok-across-surfaces.md +46 -0
- package/synergies/11-claude-code-orchestrates-aider.md +52 -0
- package/synergies/12-mcp-config-format-gotcha.md +60 -0
- package/synergies/INDEX.md +47 -0
|
@@ -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
|
+
};
|