@mcptoolshop/claude-synergy 1.1.1 → 1.2.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/dist/cli.js CHANGED
@@ -1,1169 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- AppError,
4
3
  browseChanges,
5
- embedAll,
6
4
  entityFrequency,
7
- formatError,
8
5
  getChangesSince,
9
6
  hybridSearch,
10
- initSchema,
11
7
  listProducts,
12
8
  lookupEntity,
13
- openDb,
14
9
  recentReleases,
15
10
  searchChanges
16
- } from "./chunk-H3466JDH.js";
11
+ } from "./chunk-MTW6UZBF.js";
12
+ import {
13
+ ingestAll
14
+ } from "./chunk-KFAQPOGV.js";
15
+ import {
16
+ AppError,
17
+ embedAll,
18
+ formatError,
19
+ initSchema,
20
+ openDb
21
+ } from "./chunk-CEIOLMDT.js";
17
22
  import {
18
- ingestAll,
19
- loadProductsConfig
20
- } from "./chunk-HZEQG3WT.js";
23
+ fetchAll,
24
+ seedMarkersFromDb
25
+ } from "./chunk-X25ZTSCJ.js";
26
+ import "./chunk-MZLFGICO.js";
21
27
 
22
28
  // src/cli.ts
23
29
  import { Command, Option } from "commander";
24
- import { join as join3, resolve as resolve2 } from "path";
25
- import { existsSync as existsSync2 } from "fs";
30
+ import { join, resolve } from "path";
31
+ import { existsSync } from "fs";
26
32
  import { createRequire } from "module";
27
-
28
- // src/fetch.ts
29
- import { execFileSync } from "child_process";
30
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
31
- import { join as join2, resolve } from "path";
32
-
33
- // src/fetch-rss.ts
34
- import { XMLParser } from "fast-xml-parser";
35
-
36
- // src/fetch-utils.ts
37
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
38
- var GITHUB_TOKEN_HINT = "GitHub API rate limit hit. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated).";
39
- async function fetchWithRetry(url, opts = {}) {
40
- const {
41
- timeoutMs = 3e4,
42
- maxRetries = 3,
43
- baseDelayMs = 1e3,
44
- signal: externalSignal,
45
- ...fetchInit
46
- } = opts;
47
- let lastError;
48
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
49
- if (externalSignal?.aborted) {
50
- throw new Error(`fetch aborted: ${externalSignal.reason?.message ?? "signal aborted"}`);
51
- }
52
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
53
- const combinedSignal = externalSignal ? AbortSignal.any([timeoutSignal, externalSignal]) : timeoutSignal;
54
- try {
55
- const res = await fetch(url, {
56
- ...fetchInit,
57
- signal: combinedSignal
58
- });
59
- if (res.ok) return res;
60
- if (!RETRYABLE_STATUSES.has(res.status)) {
61
- if (res.status === 403 && isGitHubApiUrl(url)) {
62
- throw new Error(`HTTP 403 from ${url} \u2014 ${GITHUB_TOKEN_HINT}`);
63
- }
64
- return res;
65
- }
66
- if (attempt < maxRetries) {
67
- const delay = computeDelay(res, attempt, baseDelayMs);
68
- console.error(
69
- `[retry] ${fetchInit.method ?? "GET"} ${String(url)} (attempt ${attempt + 2}/${maxRetries + 1}, status ${res.status}, backoff ${delay}ms)`
70
- );
71
- await sleep(delay, externalSignal);
72
- continue;
73
- }
74
- const ghHint = res.status === 429 && isGitHubApiUrl(url) ? ` ${GITHUB_TOKEN_HINT}` : "";
75
- throw new Error(`HTTP ${res.status} from ${url} after ${maxRetries + 1} attempts.${ghHint}`);
76
- } catch (e) {
77
- if (externalSignal?.aborted) {
78
- throw new Error(`fetch aborted: ${externalSignal.reason?.message ?? "signal aborted"}`);
79
- }
80
- lastError = e;
81
- if (attempt < maxRetries) {
82
- const delay = computeDelay(null, attempt, baseDelayMs);
83
- console.error(
84
- `[retry] ${fetchInit.method ?? "GET"} ${String(url)} (attempt ${attempt + 2}/${maxRetries + 1}, ${e.name ?? "Error"}: ${e.message}, backoff ${delay}ms)`
85
- );
86
- try {
87
- await sleep(delay, externalSignal);
88
- } catch {
89
- throw e;
90
- }
91
- continue;
92
- }
93
- throw e;
94
- }
95
- }
96
- throw lastError ?? new Error(`fetchWithRetry: unexpected exhaustion for ${url}`);
97
- }
98
- function computeDelay(res, attempt, baseDelayMs) {
99
- if (res) {
100
- const retryAfter = res.headers.get("retry-after");
101
- if (retryAfter) {
102
- const seconds = Number(retryAfter);
103
- if (!Number.isNaN(seconds) && seconds > 0) {
104
- return Math.min(seconds * 1e3, 6e4);
105
- }
106
- const date = new Date(retryAfter);
107
- if (!Number.isNaN(date.getTime())) {
108
- const delayMs = date.getTime() - Date.now();
109
- if (delayMs > 0) return Math.min(delayMs, 6e4);
110
- }
111
- }
112
- }
113
- const exponential = baseDelayMs * Math.pow(2, attempt);
114
- const jitter = Math.random() * baseDelayMs * 0.5;
115
- return Math.min(exponential + jitter, 3e4);
116
- }
117
- function sleep(ms, signal) {
118
- if (signal?.aborted) return Promise.reject(new Error("sleep interrupted: signal already aborted"));
119
- if (!signal) return new Promise((resolve3) => setTimeout(resolve3, ms));
120
- return new Promise((resolve3, reject) => {
121
- let settled = false;
122
- const onAbort = () => {
123
- if (settled) return;
124
- settled = true;
125
- clearTimeout(timer);
126
- reject(new Error("sleep interrupted: signal aborted"));
127
- };
128
- const timer = setTimeout(() => {
129
- if (settled) return;
130
- settled = true;
131
- signal.removeEventListener("abort", onAbort);
132
- resolve3();
133
- }, ms);
134
- signal.addEventListener("abort", onAbort, { once: true });
135
- });
136
- }
137
- function isGitHubApiUrl(url) {
138
- try {
139
- const hostname = typeof url === "string" ? new URL(url).hostname : url.hostname;
140
- return hostname === "api.github.com" || hostname === "raw.githubusercontent.com";
141
- } catch {
142
- const s = String(url);
143
- return s.includes("api.github.com") || s.includes("raw.githubusercontent.com");
144
- }
145
- }
146
- var DEFAULT_FETCH_ALL_TIMEOUT_MS = 5 * 60 * 1e3;
147
- function createFetchAllController(timeoutMs = DEFAULT_FETCH_ALL_TIMEOUT_MS) {
148
- const controller = new AbortController();
149
- const timer = setTimeout(() => {
150
- controller.abort(new Error(`fetchAll global timeout exceeded (${timeoutMs}ms)`));
151
- }, timeoutMs);
152
- if (typeof timer === "object" && "unref" in timer) {
153
- timer.unref();
154
- }
155
- return { controller, signal: controller.signal };
156
- }
157
-
158
- // src/fetch-rss.ts
159
- async function fetchRssReleases(url, sinceIso, titleFilter, signal) {
160
- const res = await fetchWithRetry(url, {
161
- headers: { "user-agent": "claude-synergy/0.1.0" },
162
- signal
163
- });
164
- if (!res.ok) throw new Error(`RSS ${url} returned ${res.status}`);
165
- const xml = await res.text();
166
- const parser = new XMLParser({
167
- ignoreAttributes: false,
168
- attributeNamePrefix: "@_",
169
- cdataPropName: "__cdata",
170
- parseTagValue: false,
171
- trimValues: true
172
- });
173
- const parsed = parser.parse(xml);
174
- const rawItems = parsed?.rss?.channel?.item ?? [];
175
- const items = Array.isArray(rawItems) ? rawItems : [rawItems];
176
- const out = [];
177
- for (const it of items) {
178
- if (!it) continue;
179
- const title = pickString(it.title);
180
- if (titleFilter && title && !titleFilter.test(title)) continue;
181
- const link = pickString(it.link) ?? pickString(it.guid) ?? "";
182
- const pubDateRaw = pickString(it.pubDate) ?? pickString(it["dc:date"]) ?? "";
183
- const pubDateIso = parsePubDate(pubDateRaw);
184
- if (!pubDateIso) continue;
185
- if (pubDateIso <= sinceIso) continue;
186
- const guid = pickString(it.guid) ?? link;
187
- const slug = makeSlug(guid, pubDateIso, title);
188
- const body = pickString(it["content:encoded"]) ?? pickString(it.description) ?? null;
189
- out.push({ slug, title, link, pubDate: pubDateIso, body });
190
- }
191
- return out;
192
- }
193
- function pickString(v) {
194
- if (v == null) return null;
195
- if (typeof v === "string") return v.trim() || null;
196
- if (typeof v === "object") {
197
- const o = v;
198
- if (typeof o.__cdata === "string") return o.__cdata.trim() || null;
199
- if (typeof o["#text"] === "string") return o["#text"].trim() || null;
200
- if (typeof o.toString === "function") {
201
- const s = String(v);
202
- if (s !== "[object Object]") return s.trim() || null;
203
- }
204
- }
205
- return null;
206
- }
207
- function parsePubDate(raw) {
208
- if (!raw) return null;
209
- const d = new Date(raw);
210
- if (Number.isNaN(d.getTime())) return null;
211
- return d.toISOString();
212
- }
213
- function makeSlug(guid, isoDate, title) {
214
- try {
215
- const u = new URL(guid);
216
- const parts = u.pathname.split("/").filter(Boolean);
217
- const last = parts[parts.length - 1];
218
- if (last && /^[a-z0-9][a-z0-9._-]*$/i.test(last)) {
219
- return last.replace(/\.html?$/, "").toLowerCase();
220
- }
221
- } catch {
222
- }
223
- const date = isoDate.split("T")[0];
224
- if (title) {
225
- const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
226
- return `${date}-${titleSlug}`;
227
- }
228
- return date;
229
- }
230
-
231
- // src/fetch-changelog.ts
232
- import { execSync } from "child_process";
233
- async function fetchAiderHistory(url, sinceIso, signal) {
234
- const res = await fetchWithRetry(url, {
235
- headers: { "user-agent": "claude-synergy/0.1.0" },
236
- signal
237
- });
238
- if (!res.ok) throw new Error(`raw-changelog ${url} returned ${res.status}`);
239
- const md = await res.text();
240
- const items = parseAiderHistory(md);
241
- const releaseDates = aiderReleaseDates();
242
- for (const item of items) {
243
- const tag = `v${item.version}`;
244
- if (releaseDates.has(tag)) {
245
- item.releasedAt = releaseDates.get(tag);
246
- }
247
- }
248
- return items.filter((i) => !i.releasedAt || i.releasedAt > sinceIso);
249
- }
250
- function parseAiderHistory(md) {
251
- const lines = md.split("\n");
252
- const out = [];
253
- let currentVersion = null;
254
- let currentBody = [];
255
- const flush = () => {
256
- if (currentVersion) {
257
- out.push({
258
- version: currentVersion,
259
- releasedAt: null,
260
- body: currentBody.join("\n").trim()
261
- });
262
- }
263
- currentBody = [];
264
- };
265
- for (const line of lines) {
266
- const headerMatch = line.match(/^###\s+(?:Aider\s+)?(?:v(\d+\.\d+\.\d+(?:[-.][\w.]+)?)|main\s+branch)\s*$/i);
267
- if (headerMatch) {
268
- flush();
269
- currentVersion = headerMatch[1] ?? "main";
270
- continue;
271
- }
272
- if (currentVersion) currentBody.push(line);
273
- }
274
- flush();
275
- return out.filter((i) => i.body.length > 0 || i.version !== "main");
276
- }
277
- async function fetchKeepAChangelog(url, sinceIso, signal) {
278
- const res = await fetchWithRetry(url, {
279
- headers: { "user-agent": "claude-synergy/0.1.0" },
280
- signal
281
- });
282
- if (!res.ok) throw new Error(`raw-changelog ${url} returned ${res.status}`);
283
- const md = await res.text();
284
- const items = parseKeepAChangelog(md);
285
- return items.filter((i) => i.releasedAt !== null && i.releasedAt > sinceIso);
286
- }
287
- function parseKeepAChangelog(md) {
288
- const lines = md.split(/\r?\n/);
289
- const out = [];
290
- const versionLineRe = /^##\s+(?:\[\s*)?(v?\d[\w.+\-]*?)(?:\s*\])?\s*(?:[-–(]\s*(\d{4}-\d{2}-\d{2})\s*\)?)?\s*$/i;
291
- let currentVersion = null;
292
- let currentDate = null;
293
- let currentBody = [];
294
- const flush = () => {
295
- if (!currentVersion) return;
296
- out.push({
297
- version: currentVersion,
298
- releasedAt: currentDate,
299
- body: currentBody.join("\n").trim()
300
- });
301
- currentVersion = null;
302
- currentDate = null;
303
- currentBody = [];
304
- };
305
- for (const line of lines) {
306
- if (line.startsWith("## ")) {
307
- const match = line.match(versionLineRe);
308
- if (match) {
309
- flush();
310
- currentVersion = match[1].replace(/^v/i, "");
311
- currentDate = match[2] ?? null;
312
- continue;
313
- }
314
- flush();
315
- continue;
316
- }
317
- if (currentVersion) currentBody.push(line);
318
- }
319
- flush();
320
- return out.filter((i) => i.body.length > 0);
321
- }
322
- function aiderReleaseDates() {
323
- try {
324
- const out = execSync(`gh api "repos/Aider-AI/aider/releases?per_page=100"`, {
325
- encoding: "utf-8",
326
- maxBuffer: 10 * 1024 * 1024,
327
- stdio: ["ignore", "pipe", "pipe"]
328
- });
329
- const releases = JSON.parse(out);
330
- const map = /* @__PURE__ */ new Map();
331
- for (const r of releases) if (r.tag_name && r.published_at) map.set(r.tag_name, r.published_at);
332
- return map;
333
- } catch (e) {
334
- const stderr = (e?.stderr ?? "").toString();
335
- if (stderr.includes("403") || stderr.includes("429") || stderr.includes("rate limit")) {
336
- console.error(
337
- "[claude-synergy] GitHub API rate limit hit while fetching aider release dates. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated)."
338
- );
339
- }
340
- return /* @__PURE__ */ new Map();
341
- }
342
- }
343
-
344
- // src/fetch-html.ts
345
- import * as cheerio from "cheerio";
346
- var UA = { "user-agent": "claude-synergy/0.1.0" };
347
- async function fetchGithubCopilotBlog(sinceIso, signal) {
348
- const baseUrl = "https://github.blog/changelog/label/copilot/";
349
- const entryUrls = /* @__PURE__ */ new Set();
350
- for (let page = 1; page <= 3; page++) {
351
- const url = page === 1 ? baseUrl : `${baseUrl}page/${page}/`;
352
- try {
353
- const res = await fetchWithRetry(url, { headers: UA, signal });
354
- if (!res.ok) break;
355
- const html = await res.text();
356
- const $ = cheerio.load(html);
357
- $('a[href*="/changelog/"]').each((_, el) => {
358
- const href = $(el).attr("href");
359
- if (!href) return;
360
- const m = href.match(/^(?:https:\/\/github\.blog)?(\/changelog\/20\d{2}-\d{2}-\d{2}-[^/?#]+\/?)$/);
361
- if (m) entryUrls.add(`https://github.blog${m[1].startsWith("/") ? m[1] : "/" + m[1]}`);
362
- });
363
- } catch {
364
- break;
365
- }
366
- }
367
- const items = [];
368
- for (const url of entryUrls) {
369
- const slugMatch = url.match(/\/changelog\/(20\d{2}-\d{2}-\d{2})-([^/]+)\/?$/);
370
- if (!slugMatch) continue;
371
- const dateStr = slugMatch[1];
372
- const pubDate = (/* @__PURE__ */ new Date(`${dateStr}T12:00:00Z`)).toISOString();
373
- if (pubDate <= sinceIso) continue;
374
- try {
375
- const res = await fetchWithRetry(url, { headers: UA, signal });
376
- if (!res.ok) continue;
377
- const html = await res.text();
378
- const $ = cheerio.load(html);
379
- const title = ($("h1").first().text() || $('meta[property="og:title"]').attr("content") || "").trim();
380
- const bodyEl = $("article").first();
381
- const body = (bodyEl.length ? bodyEl.html() : $("main").first().html()) ?? "";
382
- items.push({
383
- slug: `${dateStr}-${slugMatch[2]}`,
384
- title,
385
- pubDate,
386
- link: url,
387
- body: body.trim()
388
- });
389
- } catch {
390
- continue;
391
- }
392
- }
393
- return items;
394
- }
395
- async function fetchWindsurfChangelog(sinceIso, signal) {
396
- const url = "https://windsurf.com/changelog";
397
- const res = await fetchWithRetry(url, { headers: UA, signal });
398
- if (!res.ok) throw new Error(`Windsurf ${res.status}`);
399
- const html = await res.text();
400
- const $ = cheerio.load(html);
401
- const nextDataScript = $("script#__NEXT_DATA__").html();
402
- if (!nextDataScript) {
403
- console.error("[fetch-html] windsurf: __NEXT_DATA__ not found; changelog is client-rendered. Use the playwright strategy (src/fetch-playwright.ts) for full coverage. 0 entries.");
404
- return [];
405
- }
406
- try {
407
- const data = JSON.parse(nextDataScript);
408
- const entries = findChangelogEntries(data);
409
- if (!entries || entries.length === 0) {
410
- console.error("[fetch-html] windsurf: __NEXT_DATA__ present but no entry array found. 0 entries.");
411
- return [];
412
- }
413
- const items = [];
414
- for (const e of entries) {
415
- const title = (e.title ?? e.heading ?? e.name ?? "").toString().trim();
416
- const dateRaw = (e.date ?? e.publishedAt ?? e.published_at ?? "").toString();
417
- if (!title || !dateRaw) continue;
418
- const d = new Date(dateRaw);
419
- if (Number.isNaN(d.getTime())) continue;
420
- const pubDate = d.toISOString();
421
- if (pubDate <= sinceIso) continue;
422
- const body = (e.body ?? e.content ?? e.description ?? "").toString();
423
- const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
424
- const slug = `${pubDate.split("T")[0]}-${titleSlug || "entry"}`;
425
- items.push({ slug, title, pubDate, link: `${url}#${titleSlug}`, body });
426
- }
427
- return items;
428
- } catch (e) {
429
- console.error(`[fetch-html] windsurf: __NEXT_DATA__ parse failed: ${e.message}`);
430
- return [];
431
- }
432
- }
433
- function findChangelogEntries(obj, depth = 0) {
434
- if (depth > 8 || !obj || typeof obj !== "object") return null;
435
- if (Array.isArray(obj)) {
436
- if (obj.length > 0 && typeof obj[0] === "object" && obj[0] !== null && (typeof obj[0].title === "string" || typeof obj[0].heading === "string") && (typeof obj[0].date === "string" || typeof obj[0].publishedAt === "string" || typeof obj[0].published_at === "string")) {
437
- return obj;
438
- }
439
- return null;
440
- }
441
- for (const v of Object.values(obj)) {
442
- const found = findChangelogEntries(v, depth + 1);
443
- if (found) return found;
444
- }
445
- return null;
446
- }
447
- var VSCODE_V_TO_MONTH = {
448
- // version → [year, month-1 (0-indexed)]
449
- 100: [2025, 3],
450
- // April 2025
451
- 101: [2025, 4],
452
- // May 2025
453
- 102: [2025, 5],
454
- // June 2025
455
- 103: [2025, 6],
456
- // July 2025
457
- 104: [2025, 7],
458
- // August 2025
459
- 105: [2025, 8],
460
- // September 2025
461
- 106: [2025, 9],
462
- // October 2025
463
- 107: [2025, 10],
464
- // November 2025
465
- 108: [2025, 11],
466
- // December 2025
467
- 109: [2026, 0],
468
- // January 2026
469
- 110: [2026, 1],
470
- // February 2026
471
- 111: [2026, 2],
472
- // March 2026
473
- 112: [2026, 3],
474
- // April 2026
475
- 113: [2026, 4],
476
- // May 2026
477
- 114: [2026, 5],
478
- // June 2026 (future / Insiders)
479
- 115: [2026, 6],
480
- // July 2026 (future / Insiders)
481
- 116: [2026, 7],
482
- 117: [2026, 8],
483
- 118: [2026, 9],
484
- 119: [2026, 10],
485
- 120: [2026, 11]
486
- };
487
- function parseVscodeReleaseDate(html, $) {
488
- const releaseDateMatch = html.match(
489
- /Release\s+date[:\s]*((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},\s*\d{4})/i
490
- );
491
- if (releaseDateMatch) {
492
- const d = new Date(releaseDateMatch[1]);
493
- if (!Number.isNaN(d.getTime())) return d.toISOString();
494
- }
495
- const timeEl = $("time[datetime]").first();
496
- if (timeEl.length) {
497
- const iso = timeEl.attr("datetime");
498
- if (iso) {
499
- const d = new Date(iso);
500
- if (!Number.isNaN(d.getTime())) return d.toISOString();
501
- }
502
- }
503
- const welcomeMatch = html.match(
504
- /Welcome to the\s+((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{4})/i
505
- );
506
- if (welcomeMatch) {
507
- const d = /* @__PURE__ */ new Date(`${welcomeMatch[1]} 15`);
508
- if (!Number.isNaN(d.getTime())) return d.toISOString();
509
- }
510
- return null;
511
- }
512
- async function fetchVscodeUpdates(sinceIso, signal) {
513
- const items = [];
514
- for (const [vStr, [year, monthIdx]] of Object.entries(VSCODE_V_TO_MONTH)) {
515
- const v = parseInt(vStr, 10);
516
- const fallbackIsoDate = new Date(Date.UTC(year, monthIdx, 15, 12)).toISOString();
517
- const versionTag = `v1_${v}`;
518
- const url = `https://code.visualstudio.com/updates/${versionTag}`;
519
- const fallbackPlus31d = new Date(Date.parse(fallbackIsoDate) + 31 * 24 * 3600 * 1e3).toISOString();
520
- if (fallbackPlus31d <= sinceIso) continue;
521
- try {
522
- const res = await fetchWithRetry(url, { headers: UA, signal, maxRetries: 1 });
523
- if (res.status === 404) continue;
524
- if (!res.ok) continue;
525
- const html = await res.text();
526
- const $ = cheerio.load(html);
527
- let isoDate = parseVscodeReleaseDate(html, $);
528
- if (!isoDate) {
529
- console.warn(
530
- `[fetch-html] vscode ${versionTag}: no Release-date line found; falling back to mid-month (${fallbackIsoDate.split("T")[0]})`
531
- );
532
- isoDate = fallbackIsoDate;
533
- }
534
- if (isoDate <= sinceIso) continue;
535
- const title = $("h1").first().text().trim() || `Visual Studio Code 1.${v}`;
536
- const body = ($("main").first().html() || $("article").first().html() || "").trim();
537
- if (body.length < 200) continue;
538
- items.push({
539
- slug: versionTag,
540
- title,
541
- pubDate: isoDate,
542
- link: url,
543
- body
544
- });
545
- } catch {
546
- continue;
547
- }
548
- }
549
- return items;
550
- }
551
- async function fetchHtmlReleases(parser, sinceIso, signal) {
552
- switch (parser) {
553
- case "github-copilot-blog":
554
- return fetchGithubCopilotBlog(sinceIso, signal);
555
- case "windsurf-changelog":
556
- return fetchWindsurfChangelog(sinceIso, signal);
557
- case "vscode-updates":
558
- return fetchVscodeUpdates(sinceIso, signal);
559
- }
560
- }
561
-
562
- // src/fetch-mcp-registry.ts
563
- import { writeFileSync, mkdirSync } from "fs";
564
- import { join } from "path";
565
- var UA2 = { "user-agent": "claude-synergy/0.1.0", accept: "application/json" };
566
- async function fetchOfficialMcpRegistry(opts = {}) {
567
- const maxPages = opts.maxPages ?? 50;
568
- const limit = 100;
569
- let cursor;
570
- const byName = /* @__PURE__ */ new Map();
571
- for (let page = 0; page < maxPages; page++) {
572
- const url = `https://registry.modelcontextprotocol.io/v0/servers?limit=${limit}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
573
- const res = await fetchWithRetry(url, { headers: UA2, signal: opts.signal });
574
- if (!res.ok) throw new Error(`Official MCP Registry ${res.status}`);
575
- const json = await res.json();
576
- if (!Array.isArray(json.servers)) break;
577
- for (const item of json.servers) {
578
- const s = item.server;
579
- if (!s?.name) continue;
580
- const meta = item._meta?.["io.modelcontextprotocol.registry/official"];
581
- const isLatest = meta?.isLatest === true;
582
- const existing = byName.get(s.name);
583
- if (existing && !isLatest) continue;
584
- byName.set(s.name, {
585
- slug: slugify(s.name),
586
- name: s.title ?? s.name,
587
- description: (s.description ?? "").trim(),
588
- homepage: s.remotes?.[0]?.url ?? null,
589
- popularity: null,
590
- createdAt: meta?.publishedAt ?? null,
591
- source: "official-mcp-registry",
592
- category: null,
593
- verified: null
594
- });
595
- }
596
- const next = json.metadata?.nextCursor;
597
- if (!next || next === cursor) break;
598
- cursor = next;
599
- }
600
- return [...byName.values()];
601
- }
602
- async function fetchSmitheryRegistry(opts = {}) {
603
- const pageSize = opts.pageSize ?? 100;
604
- const maxEntries = opts.maxEntries ?? 200;
605
- const out = [];
606
- let page = 1;
607
- while (out.length < maxEntries) {
608
- const url = `https://registry.smithery.ai/servers?page=${page}&pageSize=${pageSize}`;
609
- const res = await fetchWithRetry(url, { headers: UA2, signal: opts.signal });
610
- if (!res.ok) throw new Error(`Smithery Registry ${res.status}`);
611
- const json = await res.json();
612
- if (!Array.isArray(json.servers) || json.servers.length === 0) break;
613
- for (const s of json.servers) {
614
- if (out.length >= maxEntries) break;
615
- out.push({
616
- slug: slugify(s.qualifiedName ?? s.namespace ?? s.id),
617
- name: s.displayName ?? s.qualifiedName,
618
- description: (s.description ?? "").trim(),
619
- homepage: s.homepage ?? null,
620
- popularity: typeof s.useCount === "number" ? s.useCount : null,
621
- createdAt: s.createdAt ?? null,
622
- source: "smithery",
623
- category: null,
624
- verified: s.verified ?? null
625
- });
626
- }
627
- const total = json.pagination?.totalPages ?? 1;
628
- if (page >= total) break;
629
- page++;
630
- }
631
- out.sort((a, b) => (b.popularity ?? 0) - (a.popularity ?? 0));
632
- return out;
633
- }
634
- function writeCatalog(productDir, productName, entries) {
635
- mkdirSync(productDir, { recursive: true });
636
- const catalogPath = join(productDir, "CATALOG.md");
637
- const lines = [];
638
- lines.push(`# ${productName} \u2014 MCP server catalog`);
639
- lines.push("");
640
- lines.push(`**Source:** ${entries[0]?.source ?? "unknown"}`);
641
- lines.push(`**Entries:** ${entries.length}`);
642
- lines.push(`**Fetched:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
643
- lines.push("");
644
- lines.push("| Name | Description | Popularity | Created | Homepage |");
645
- lines.push("|------|-------------|------------|---------|----------|");
646
- for (const e of entries) {
647
- const desc = (e.description.split("\n")[0] ?? "").slice(0, 160).replace(/\|/g, "\\|");
648
- const pop = e.popularity != null ? String(e.popularity) : "\u2014";
649
- const date = e.createdAt ? e.createdAt.split("T")[0] : "\u2014";
650
- const home = e.homepage ? `[link](${e.homepage})` : "\u2014";
651
- const name = e.name.replace(/\|/g, "\\|");
652
- lines.push(`| ${name} | ${desc} | ${pop} | ${date} | ${home} |`);
653
- }
654
- lines.push("");
655
- writeFileSync(catalogPath, lines.join("\n"), "utf-8");
656
- return {
657
- source: entries[0]?.source ?? "unknown",
658
- entriesFetched: entries.length,
659
- productDir,
660
- catalogPath
661
- };
662
- }
663
- function slugify(s) {
664
- return s.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
665
- }
666
- async function fetchCatalog(catalogType, opts = {}) {
667
- switch (catalogType) {
668
- case "official-mcp-registry":
669
- return fetchOfficialMcpRegistry({ maxPages: opts.maxPages, signal: opts.signal });
670
- case "smithery":
671
- return fetchSmitheryRegistry({ maxEntries: opts.maxEntries, signal: opts.signal });
672
- }
673
- }
674
-
675
- // src/fetch.ts
676
- var REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
677
- var PRODUCT_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
678
- function assertRepoShape(repo) {
679
- if (!REPO_PATTERN.test(repo)) {
680
- throw new Error(
681
- `invalid repo: ${JSON.stringify(repo)} \u2014 expected "<org>/<repo>" with safe chars [A-Za-z0-9._-]`
682
- );
683
- }
684
- }
685
- function assertProductShape(product) {
686
- if (!PRODUCT_PATTERN.test(product)) {
687
- throw new Error(
688
- `invalid product name: ${JSON.stringify(product)} \u2014 expected [a-z0-9][a-z0-9-]*`
689
- );
690
- }
691
- }
692
- function sanitizeFilename(s) {
693
- const cleaned = s.replace(/[\/\\]/g, "-").replace(/^\.+/, "").replace(/\.\./g, "-").replace(/[<>:"|?*\x00-\x1f]/g, "-").slice(0, 100);
694
- if (cleaned.includes("..") || cleaned.includes("/") || cleaned.includes("\\")) {
695
- throw new Error(`sanitizeFilename produced unsafe output: ${JSON.stringify(cleaned)}`);
696
- }
697
- return cleaned;
698
- }
699
- function assertSafeFilename(name, context) {
700
- if (!name || name.includes("..") || name.includes("/") || name.includes("\\")) {
701
- throw new Error(`unsafe filename in ${context}: ${JSON.stringify(name)}`);
702
- }
703
- }
704
- var HARDCODED_FALLBACK_TARGETS = [
705
- // ── Existing Anthropic GH-Releases sources ────────────────────────────────
706
- // FE-1: claude-code is the flagship product — wired to gh-releases because the
707
- // repo publishes Releases whose bodies mirror CHANGELOG.md but carry real dates.
708
- { product: "claude-code", strategy: "gh-releases", repo: "anthropics/claude-code" },
709
- { product: "claude-agent-sdk-python", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-python" },
710
- { product: "claude-agent-sdk-typescript", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-typescript" },
711
- { product: "anthropic-cli", strategy: "gh-releases", repo: "anthropics/anthropic-cli" },
712
- { product: "anthropic-sdk-python", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-python" },
713
- { product: "anthropic-sdk-typescript", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-typescript", multiPackage: true },
714
- { product: "anthropic-sdk-go", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-go" },
715
- { product: "anthropic-sdk-java", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-java" },
716
- { product: "anthropic-sdk-ruby", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-ruby" },
717
- { product: "anthropic-sdk-csharp", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-csharp", multiPackage: true },
718
- { product: "anthropic-sdk-php", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-php" },
719
- { product: "claude-code-action", strategy: "gh-releases", repo: "anthropics/claude-code-action" },
720
- { product: "claude-code-security-review", strategy: "gh-releases", repo: "anthropics/claude-code-security-review" },
721
- // ── Tier 4a additions ─────────────────────────────────────────────────────
722
- // MCP ecosystem SDKs (modelcontextprotocol/*)
723
- { product: "mcp-python-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/python-sdk" },
724
- { product: "mcp-typescript-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/typescript-sdk", multiPackage: true },
725
- { product: "mcp-go-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/go-sdk" },
726
- { product: "mcp-java-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/java-sdk" },
727
- { product: "mcp-csharp-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/csharp-sdk" },
728
- { product: "mcp-kotlin-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/kotlin-sdk" },
729
- { product: "mcp-ruby-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/ruby-sdk" },
730
- { product: "mcp-swift-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/swift-sdk" },
731
- { product: "mcp-rust-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/rust-sdk" },
732
- { product: "mcp-php-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/php-sdk" },
733
- { product: "mcp-spec", strategy: "gh-releases", repo: "modelcontextprotocol/modelcontextprotocol" },
734
- { product: "mcp-inspector", strategy: "gh-releases", repo: "modelcontextprotocol/inspector" },
735
- { product: "mcp-registry", strategy: "gh-releases", repo: "modelcontextprotocol/registry" },
736
- { product: "mcp-mcpb", strategy: "gh-releases", repo: "modelcontextprotocol/mcpb" },
737
- { product: "mcp-conformance", strategy: "gh-releases", repo: "modelcontextprotocol/conformance" },
738
- // Continue.dev (multi-platform tagged releases)
739
- { product: "continue-dev", strategy: "gh-releases", repo: "continuedev/continue", multiPackage: true },
740
- { product: "continue-cli", strategy: "gh-releases", repo: "continuedev/continue-cli" },
741
- // RSS-based feeds
742
- { product: "cursor", strategy: "rss", rssUrl: "https://cursor.com/changelog/rss.xml" },
743
- { product: "cody-enterprise", strategy: "rss", rssUrl: "https://sourcegraph.com/changelog/featured.rss", rssTitleFilter: /cody|sourcegraph/i },
744
- // Raw markdown CHANGELOG
745
- { product: "aider", strategy: "raw-changelog", rawChangelogUrl: "https://raw.githubusercontent.com/Aider-AI/aider/main/HISTORY.md", rawChangelogParser: "aider-history" },
746
- // ── Tier 4b additions — HTML-scraped sources ──────────────────────────────
747
- { product: "github-copilot", strategy: "html-scrape", htmlParser: "github-copilot-blog" },
748
- { product: "vscode-copilot-chat", strategy: "html-scrape", htmlParser: "vscode-updates" },
749
- { product: "windsurf", strategy: "html-scrape", htmlParser: "windsurf-changelog" }
750
- ];
751
- var TARGETS = loadProductsConfig()?.fetchTargets ?? HARDCODED_FALLBACK_TARGETS;
752
- async function fetchAll(db, productsRoot, opts = {}) {
753
- const targets = opts.product ? TARGETS.filter((t) => t.product === opts.product) : TARGETS;
754
- if (targets.length === 0) {
755
- throw new Error(`unknown product: ${opts.product} (available: ${TARGETS.map((t) => t.product).join(", ")})`);
756
- }
757
- const total = targets.length;
758
- const onProgress = opts.onProgress;
759
- const globalTimeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_ALL_TIMEOUT_MS;
760
- const { controller, signal } = createFetchAllController(globalTimeoutMs);
761
- const results = [];
762
- try {
763
- for (let i = 0; i < targets.length; i++) {
764
- const target = targets[i];
765
- if (signal.aborted) {
766
- onProgress?.({ type: "skip", product: target.product, index: i, total, error: "global timeout exceeded" });
767
- results.push({
768
- product: target.product,
769
- fetched: 0,
770
- newSince: opts.sinceOverride ?? "",
771
- latest: null,
772
- errors: ["skipped: global timeout exceeded"]
773
- });
774
- continue;
775
- }
776
- onProgress?.({ type: "start", product: target.product, index: i, total });
777
- const stats = await fetchOne(db, productsRoot, target, opts.sinceOverride, signal);
778
- results.push(stats);
779
- if (stats.errors.length > 0) {
780
- onProgress?.({ type: "error", product: target.product, index: i, total, error: stats.errors[0] });
781
- } else {
782
- onProgress?.({ type: "done", product: target.product, index: i, total });
783
- }
784
- }
785
- } finally {
786
- controller.abort();
787
- }
788
- const augmented = results;
789
- augmented.summary = buildSummary(results);
790
- return augmented;
791
- }
792
- function buildSummary(results) {
793
- let succeeded = 0;
794
- let failed = 0;
795
- let skipped = 0;
796
- let newChanges = 0;
797
- const errors = [];
798
- for (const r of results) {
799
- const isSkipped = r.errors.some((e) => e.startsWith("skipped:"));
800
- const isFailed = r.errors.length > 0 && !isSkipped;
801
- if (isSkipped) {
802
- skipped++;
803
- } else if (isFailed) {
804
- failed++;
805
- for (const e of r.errors) {
806
- errors.push({ product: r.product, error: e });
807
- }
808
- } else {
809
- succeeded++;
810
- }
811
- newChanges += r.fetched;
812
- }
813
- return { total: results.length, succeeded, failed, skipped, newChanges, errors };
814
- }
815
- async function fetchOne(db, productsRoot, target, sinceOverride, signal) {
816
- assertProductShape(target.product);
817
- const since = sinceOverride ?? readMarker(db, target.product) ?? "2026-01-01";
818
- const outDir = resolve(productsRoot, target.product, "releases");
819
- mkdirSync2(outDir, { recursive: true });
820
- try {
821
- switch (target.strategy) {
822
- case "gh-releases":
823
- return await fetchGhReleases(db, outDir, target, since);
824
- case "rss":
825
- return await fetchRss(db, outDir, target, since, signal);
826
- case "raw-changelog":
827
- return await fetchRawChangelog(db, outDir, target, since, signal);
828
- case "html-scrape":
829
- return await fetchHtmlScrape(db, outDir, target, since, signal);
830
- case "catalog":
831
- return await fetchCatalogStrategy(db, productsRoot, target, since, signal);
832
- case "playwright":
833
- return await fetchPlaywrightStrategy(db, outDir, target, since);
834
- }
835
- } catch (e) {
836
- return {
837
- product: target.product,
838
- fetched: 0,
839
- newSince: since,
840
- latest: null,
841
- errors: [`fetch failed: ${e.message}`]
842
- };
843
- }
844
- }
845
- async function fetchGhReleases(db, outDir, target, since) {
846
- if (!target.repo) throw new Error(`${target.product}: gh-releases strategy requires repo`);
847
- const releases = ghReleases(target.repo, since);
848
- let latest = null;
849
- let fetched = 0;
850
- const errors = [];
851
- for (const r of releases) {
852
- try {
853
- const baseName = filenameFor(target, r.tag_name);
854
- const filename = sanitizeFilename(baseName);
855
- assertSafeFilename(filename, `gh-releases filename for tag ${JSON.stringify(r.tag_name)}`);
856
- const vName = sanitizeFilename(`v${baseName}`);
857
- assertSafeFilename(vName, `gh-releases vName for tag ${JSON.stringify(r.tag_name)}`);
858
- const path = join2(outDir, `${filename}.md`);
859
- const vPath = join2(outDir, `${vName}.md`);
860
- if (existsSync(path) || existsSync(vPath)) {
861
- if (!latest || r.published_at > latest) latest = r.published_at;
862
- continue;
863
- }
864
- writeFileSync2(path, renderGhRelease(target.product, r), "utf-8");
865
- fetched++;
866
- if (!latest || r.published_at > latest) latest = r.published_at;
867
- } catch (e) {
868
- errors.push(`${r.tag_name}: ${e.message}`);
869
- }
870
- }
871
- if (latest) writeMarker(db, target.product, latest, target.strategy);
872
- return { product: target.product, fetched, newSince: since, latest, errors };
873
- }
874
- function ghReleases(repo, sinceIso) {
875
- assertRepoShape(repo);
876
- const all = [];
877
- const MAX_PAGES = 50;
878
- for (let page = 1; page <= MAX_PAGES; page++) {
879
- let out;
880
- try {
881
- out = execFileSync(
882
- "gh",
883
- ["api", `repos/${repo}/releases?per_page=100&page=${page}`],
884
- { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }
885
- );
886
- } catch (e) {
887
- const stderr = (e.stderr ?? "").toString();
888
- if (stderr.includes("404")) return page === 1 ? [] : filterAndShape(all, sinceIso);
889
- if (stderr.includes("403") || stderr.includes("429") || stderr.includes("rate limit")) {
890
- throw new Error(
891
- `GitHub API rate limit hit for ${repo}. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated). Original error: ${e.message}`
892
- );
893
- }
894
- throw e;
895
- }
896
- let batch;
897
- try {
898
- batch = JSON.parse(out);
899
- } catch {
900
- return page === 1 ? [] : filterAndShape(all, sinceIso);
901
- }
902
- if (!Array.isArray(batch) || batch.length === 0) break;
903
- all.push(...batch);
904
- if (batch.length === 100) {
905
- const lastPubAt = batch[batch.length - 1]?.published_at;
906
- if (lastPubAt && lastPubAt <= sinceIso) break;
907
- }
908
- if (batch.length < 100) break;
909
- }
910
- return filterAndShape(all, sinceIso);
911
- }
912
- function filterAndShape(all, sinceIso) {
913
- return all.filter((r) => r.published_at && r.published_at > sinceIso).map((r) => ({
914
- tag_name: r.tag_name,
915
- published_at: r.published_at,
916
- name: r.name,
917
- body: r.body,
918
- html_url: r.html_url
919
- }));
920
- }
921
- function filenameFor(target, tag) {
922
- if (target.multiPackage) {
923
- return tag.replace(/^@/, "").replace(/\//g, "-").replace(/@/g, "-").replace(/-v(\d)/g, "-$1").replace(/^v/, "");
924
- }
925
- return tag.replace(/^v/, "");
926
- }
927
- function yamlQuote(s) {
928
- if (s == null) return "";
929
- return s.replace(/[\x00-\x1f]/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
930
- }
931
- function renderGhRelease(product, r) {
932
- const version = (r.tag_name ?? "").replace(/^v/, "");
933
- return [
934
- "---",
935
- `product: ${product}`,
936
- `version: "${yamlQuote(version)}"`,
937
- `released_at: "${r.published_at.split("T")[0]}"`,
938
- `source_url: "${yamlQuote(r.html_url)}"`,
939
- `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
940
- "---",
941
- "",
942
- `# ${product} ${r.tag_name}`,
943
- "",
944
- r.body ?? "(no body)",
945
- ""
946
- ].join("\n");
947
- }
948
- async function fetchRss(db, outDir, target, since, signal) {
949
- if (!target.rssUrl) throw new Error(`${target.product}: rss strategy requires rssUrl`);
950
- const items = await fetchRssReleases(target.rssUrl, since, target.rssTitleFilter, signal);
951
- let latest = null;
952
- let fetched = 0;
953
- const errors = [];
954
- for (const item of items) {
955
- try {
956
- const safeSlug = sanitizeFilename(item.slug);
957
- assertSafeFilename(safeSlug, `rss slug for ${JSON.stringify(item.slug)}`);
958
- const path = join2(outDir, `${safeSlug}.md`);
959
- if (existsSync(path)) {
960
- if (!latest || item.pubDate > latest) latest = item.pubDate;
961
- continue;
962
- }
963
- const body = [
964
- "---",
965
- `product: ${target.product}`,
966
- `version: "${yamlQuote(safeSlug)}"`,
967
- `released_at: "${item.pubDate.split("T")[0]}"`,
968
- `source_url: "${yamlQuote(item.link)}"`,
969
- `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
970
- `title: "${yamlQuote(item.title ?? "")}"`,
971
- "---",
972
- "",
973
- `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
974
- "",
975
- item.body ?? "(no body)",
976
- ""
977
- ].join("\n");
978
- writeFileSync2(path, body, "utf-8");
979
- fetched++;
980
- if (!latest || item.pubDate > latest) latest = item.pubDate;
981
- } catch (e) {
982
- errors.push(`${item.slug}: ${e.message}`);
983
- }
984
- }
985
- if (latest) writeMarker(db, target.product, latest, target.strategy);
986
- return { product: target.product, fetched, newSince: since, latest, errors };
987
- }
988
- async function fetchRawChangelog(db, outDir, target, since, signal) {
989
- if (!target.rawChangelogUrl) throw new Error(`${target.product}: raw-changelog requires url`);
990
- let items;
991
- switch (target.rawChangelogParser) {
992
- case "aider-history":
993
- items = await fetchAiderHistory(target.rawChangelogUrl, since, signal);
994
- break;
995
- case "keep-a-changelog":
996
- items = await fetchKeepAChangelog(target.rawChangelogUrl, since, signal);
997
- break;
998
- default:
999
- throw new Error(`${target.product}: unsupported parser ${target.rawChangelogParser}`);
1000
- }
1001
- let latest = null;
1002
- let fetched = 0;
1003
- const errors = [];
1004
- for (const item of items) {
1005
- try {
1006
- const safeVersion = sanitizeFilename(item.version);
1007
- assertSafeFilename(safeVersion, `raw-changelog version for ${JSON.stringify(item.version)}`);
1008
- const path = join2(outDir, `${safeVersion}.md`);
1009
- if (existsSync(path)) {
1010
- if (item.releasedAt && (!latest || item.releasedAt > latest)) latest = item.releasedAt;
1011
- continue;
1012
- }
1013
- const body = [
1014
- "---",
1015
- `product: ${target.product}`,
1016
- `version: "${yamlQuote(safeVersion)}"`,
1017
- `released_at: ${item.releasedAt ? `"${yamlQuote(item.releasedAt)}"` : "null"}`,
1018
- `source_url: "${yamlQuote(target.rawChangelogUrl ?? "")}"`,
1019
- `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1020
- "---",
1021
- "",
1022
- `# ${target.product} v${item.version}`,
1023
- "",
1024
- item.body,
1025
- ""
1026
- ].join("\n");
1027
- writeFileSync2(path, body, "utf-8");
1028
- fetched++;
1029
- if (item.releasedAt && (!latest || item.releasedAt > latest)) latest = item.releasedAt;
1030
- } catch (e) {
1031
- errors.push(`${item.version}: ${e.message}`);
1032
- }
1033
- }
1034
- if (latest) writeMarker(db, target.product, latest, target.strategy);
1035
- return { product: target.product, fetched, newSince: since, latest, errors };
1036
- }
1037
- async function fetchHtmlScrape(db, outDir, target, since, signal) {
1038
- if (!target.htmlParser) throw new Error(`${target.product}: html-scrape requires htmlParser`);
1039
- const items = await fetchHtmlReleases(target.htmlParser, since, signal);
1040
- let latest = null;
1041
- let fetched = 0;
1042
- const errors = [];
1043
- for (const item of items) {
1044
- try {
1045
- const safeSlug = sanitizeFilename(item.slug);
1046
- assertSafeFilename(safeSlug, `html-scrape slug for ${JSON.stringify(item.slug)}`);
1047
- const path = join2(outDir, `${safeSlug}.md`);
1048
- if (existsSync(path)) {
1049
- if (!latest || item.pubDate > latest) latest = item.pubDate;
1050
- continue;
1051
- }
1052
- const body = [
1053
- "---",
1054
- `product: ${target.product}`,
1055
- `version: "${yamlQuote(safeSlug)}"`,
1056
- `released_at: "${item.pubDate.split("T")[0]}"`,
1057
- `source_url: "${yamlQuote(item.link)}"`,
1058
- `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1059
- `title: "${yamlQuote(item.title ?? "")}"`,
1060
- "---",
1061
- "",
1062
- `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
1063
- "",
1064
- item.body ?? "(no body)",
1065
- ""
1066
- ].join("\n");
1067
- writeFileSync2(path, body, "utf-8");
1068
- fetched++;
1069
- if (!latest || item.pubDate > latest) latest = item.pubDate;
1070
- } catch (e) {
1071
- errors.push(`${item.slug}: ${e.message}`);
1072
- }
1073
- }
1074
- if (latest) writeMarker(db, target.product, latest, target.strategy);
1075
- return { product: target.product, fetched, newSince: since, latest, errors };
1076
- }
1077
- async function fetchPlaywrightStrategy(db, outDir, target, since) {
1078
- const { fetchWindsurfWithPlaywright } = await import("./fetch-playwright-HQ6OTMSQ.js");
1079
- const items = await fetchWindsurfWithPlaywright(since);
1080
- let latest = null;
1081
- let fetched = 0;
1082
- const errors = [];
1083
- for (const item of items) {
1084
- try {
1085
- const safeSlug = sanitizeFilename(item.slug);
1086
- assertSafeFilename(safeSlug, `playwright slug for ${JSON.stringify(item.slug)}`);
1087
- const path = join2(outDir, `${safeSlug}.md`);
1088
- if (existsSync(path)) {
1089
- if (!latest || item.pubDate > latest) latest = item.pubDate;
1090
- continue;
1091
- }
1092
- const body = [
1093
- "---",
1094
- `product: ${target.product}`,
1095
- `version: "${yamlQuote(safeSlug)}"`,
1096
- `released_at: "${item.pubDate.split("T")[0]}"`,
1097
- `source_url: "${yamlQuote(item.link)}"`,
1098
- `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1099
- `title: "${yamlQuote(item.title ?? "")}"`,
1100
- "---",
1101
- "",
1102
- `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
1103
- "",
1104
- item.body ?? "(no body)",
1105
- ""
1106
- ].join("\n");
1107
- writeFileSync2(path, body, "utf-8");
1108
- fetched++;
1109
- if (!latest || item.pubDate > latest) latest = item.pubDate;
1110
- } catch (e) {
1111
- errors.push(`${item.slug}: ${e.message}`);
1112
- }
1113
- }
1114
- if (latest) writeMarker(db, target.product, latest, target.strategy);
1115
- return { product: target.product, fetched, newSince: since, latest, errors };
1116
- }
1117
- async function fetchCatalogStrategy(db, productsRoot, target, since, signal) {
1118
- if (!target.catalogType) throw new Error(`${target.product}: catalog requires catalogType`);
1119
- const entries = await fetchCatalog(target.catalogType, { maxEntries: target.catalogMaxEntries, signal });
1120
- const productDir = resolve(productsRoot, target.product);
1121
- writeCatalog(productDir, target.product, entries);
1122
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1123
- writeMarker(db, target.product, nowIso, target.strategy);
1124
- return {
1125
- product: target.product,
1126
- fetched: entries.length,
1127
- newSince: since,
1128
- latest: nowIso,
1129
- errors: []
1130
- };
1131
- }
1132
- function readMarker(db, product) {
1133
- const row = db.prepare(`SELECT version FROM markers WHERE product = ? AND name = 'last_fetched_release_at'`).get(product);
1134
- return row?.version ?? null;
1135
- }
1136
- function writeMarker(db, product, isoTimestamp, fetchStrategy = "gh-releases") {
1137
- assertProductShape(product);
1138
- db.prepare(`
1139
- INSERT OR IGNORE INTO products (name, display_name, source_tier, source_url, fetch_strategy, notes)
1140
- VALUES (?, ?, 1, '', ?, NULL)
1141
- `).run(product, product, fetchStrategy);
1142
- db.prepare(`
1143
- INSERT INTO markers (product, name, version, updated_at)
1144
- VALUES (?, 'last_fetched_release_at', ?, ?)
1145
- ON CONFLICT(product, name) DO UPDATE SET version = excluded.version, updated_at = excluded.updated_at
1146
- `).run(product, isoTimestamp, (/* @__PURE__ */ new Date()).toISOString());
1147
- }
1148
- function seedMarkersFromDb(db) {
1149
- const rows = db.prepare(
1150
- `SELECT product, MAX(released_at) AS max_date FROM releases WHERE released_at IS NOT NULL GROUP BY product`
1151
- ).all();
1152
- const out = [];
1153
- for (const t of TARGETS) {
1154
- const r = rows.find((x) => x.product === t.product);
1155
- if (r?.max_date) {
1156
- const iso = r.max_date.includes("T") ? r.max_date : `${r.max_date}T23:59:59Z`;
1157
- writeMarker(db, t.product, iso, t.strategy);
1158
- out.push({ product: t.product, seededTo: iso });
1159
- } else {
1160
- out.push({ product: t.product, seededTo: null });
1161
- }
1162
- }
1163
- return out;
1164
- }
1165
-
1166
- // src/cli.ts
1167
33
  var LOG_LEVELS = { silent: 0, normal: 1, verbose: 2, debug: 3 };
1168
34
  function resolveLogLevel() {
1169
35
  const env = (process.env.HK_LOG_LEVEL ?? "").toLowerCase();
@@ -1191,8 +57,8 @@ function shutdown(signal) {
1191
57
  process.on("SIGINT", () => shutdown("SIGINT"));
1192
58
  process.on("SIGTERM", () => shutdown("SIGTERM"));
1193
59
  var cwd = process.cwd();
1194
- var DEFAULT_DB = join3(cwd, "data", "claude-synergy.db");
1195
- var DEFAULT_PRODUCTS = join3(cwd, "products");
60
+ var DEFAULT_DB = join(cwd, "data", "claude-synergy.db");
61
+ var DEFAULT_PRODUCTS = join(cwd, "products");
1196
62
  var _require = createRequire(import.meta.url);
1197
63
  var { version: PKG_VERSION } = _require("../package.json");
1198
64
  function intOpt(name, raw, defaultValue, min = 1, max = 1e4) {
@@ -1339,11 +205,11 @@ program.name("hk").description("Claude Synergy \u2014 local Anthropic changelog
1339
205
  program.command("init").description("Create the database file with the current schema").option("-d, --db <path>", "database path", DEFAULT_DB).action((opts) => {
1340
206
  const db = openTrackedDb(opts.db);
1341
207
  initSchema(db);
1342
- console.log(`\u2713 initialized ${resolve2(opts.db)}`);
208
+ console.log(`\u2713 initialized ${resolve(opts.db)}`);
1343
209
  db.close();
1344
210
  });
1345
211
  program.command("ingest").description("Parse products/*/releases/*.md and load into DB (idempotent)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-p, --products <path>", "products dir", DEFAULT_PRODUCTS).action((opts) => {
1346
- if (!existsSync2(opts.products)) {
212
+ if (!existsSync(opts.products)) {
1347
213
  console.error(`\u2717 products dir not found: ${opts.products}`);
1348
214
  process.exit(1);
1349
215
  }
@@ -1561,7 +427,7 @@ program.command("sync").description("Run fetch \u2192 ingest \u2192 embed in seq
1561
427
  }
1562
428
  }
1563
429
  process.stderr.write("\n=== ingest ===\n");
1564
- const { ingestAll: ingestAll2 } = await import("./ingest-Z45YH7OX.js");
430
+ const { ingestAll: ingestAll2 } = await import("./ingest-D23NTE25.js");
1565
431
  const ingestStats = ingestAll2(db, opts.productsRoot);
1566
432
  console.log(`ingested ${ingestStats.releasesAdded} releases, ${ingestStats.changesAdded} changes, ${ingestStats.entitiesAdded} entities`);
1567
433
  if (!opts.skipEmbed) {