@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/dist/cli.js ADDED
@@ -0,0 +1,2090 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OllamaEmbeddingProvider,
4
+ VoyageEmbeddingProvider,
5
+ entityFrequency,
6
+ hybridSearch,
7
+ initSchema,
8
+ listProducts,
9
+ lookupEntity,
10
+ openDb,
11
+ recentReleases,
12
+ searchChanges
13
+ } from "./chunk-YFGUTT22.js";
14
+ import {
15
+ ingestAll,
16
+ loadProductsConfig
17
+ } from "./chunk-HCIZPSW4.js";
18
+
19
+ // src/cli.ts
20
+ import { Command, Option } from "commander";
21
+ import { join as join4, resolve as resolve2 } from "path";
22
+ import { existsSync as existsSync2 } from "fs";
23
+ import { createRequire } from "module";
24
+
25
+ // src/embed.ts
26
+ import { readFileSync } from "fs";
27
+ import { fileURLToPath } from "url";
28
+ import { dirname, join } from "path";
29
+
30
+ // src/providers/context/none.ts
31
+ var NoneContextProvider = class {
32
+ name = "none";
33
+ async contextFor(_chunk, _release) {
34
+ return "";
35
+ }
36
+ };
37
+
38
+ // src/providers/context/structured.ts
39
+ var StructuredContextProvider = class {
40
+ name = "structured";
41
+ async contextFor(chunk, release) {
42
+ const date = release.releasedAt ?? "unknown date";
43
+ const positional = `change ${chunk.ordinalInRelease} of ${chunk.totalInRelease}`;
44
+ return `In ${release.product} ${release.version} (${date}), ${chunk.kind} (${positional}):`;
45
+ }
46
+ };
47
+
48
+ // src/providers/context/ollama.ts
49
+ function providerTimeoutMs() {
50
+ const raw = process.env.CLAUDE_SYNERGY_PROVIDER_TIMEOUT_MS;
51
+ const n = raw === void 0 ? NaN : parseInt(raw, 10);
52
+ return Number.isFinite(n) && n > 0 ? n : 6e4;
53
+ }
54
+ async function safeErrorBody(res, max = 200) {
55
+ try {
56
+ const body = await res.text();
57
+ const safe = body.split("\n").filter((l) => !/x-api-key|authorization|bearer|api[-_]?key/i.test(l)).join("\n");
58
+ return safe.slice(0, max);
59
+ } catch {
60
+ return "<unreadable>";
61
+ }
62
+ }
63
+ var OllamaContextProvider = class {
64
+ name = "ollama";
65
+ host;
66
+ model;
67
+ constructor(opts = {}) {
68
+ const rawHost = opts.host ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
69
+ this.host = /^https?:\/\//.test(rawHost) ? rawHost : `http://${rawHost}`;
70
+ this.model = opts.model ?? process.env.OLLAMA_GEN_MODEL ?? "llama3.2:3b";
71
+ }
72
+ async contextFor(chunk, release) {
73
+ const releaseSnippet = release.siblings.map((s, i) => `${i + 1}. ${s.text}`).slice(0, 30).join("\n");
74
+ const prompt = [
75
+ `<document>`,
76
+ `${release.product} ${release.version} release notes (${release.releasedAt ?? "unknown date"}):`,
77
+ releaseSnippet,
78
+ `</document>`,
79
+ ``,
80
+ `Here is the chunk we want to situate within the whole document:`,
81
+ `<chunk>`,
82
+ chunk.text,
83
+ `</chunk>`,
84
+ ``,
85
+ `Please give a short succinct context (1 sentence, max 30 words) to situate this chunk within the overall document for improving search retrieval of the chunk. Mention any related product names, env vars, command names, or APIs. Answer only with the succinct context and nothing else.`
86
+ ].join("\n");
87
+ const timeoutMs = providerTimeoutMs();
88
+ let res;
89
+ try {
90
+ res = await fetch(`${this.host}/api/generate`, {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json" },
93
+ body: JSON.stringify({
94
+ model: this.model,
95
+ prompt,
96
+ stream: false,
97
+ options: { temperature: 0, num_predict: 80 }
98
+ }),
99
+ signal: AbortSignal.timeout(timeoutMs)
100
+ });
101
+ } catch (e) {
102
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") {
103
+ throw new Error(`Ollama context request timed out after ${timeoutMs}ms \u2014 is the Ollama server responsive?`);
104
+ }
105
+ throw e;
106
+ }
107
+ if (!res.ok) throw new Error(`Ollama ${res.status}: ${await safeErrorBody(res)}`);
108
+ const json = await res.json();
109
+ return json.response.trim();
110
+ }
111
+ };
112
+
113
+ // src/providers/context/claude-haiku.ts
114
+ function providerTimeoutMs2() {
115
+ const raw = process.env.CLAUDE_SYNERGY_PROVIDER_TIMEOUT_MS;
116
+ const n = raw === void 0 ? NaN : parseInt(raw, 10);
117
+ return Number.isFinite(n) && n > 0 ? n : 6e4;
118
+ }
119
+ async function safeErrorBody2(res, max = 200) {
120
+ try {
121
+ const body = await res.text();
122
+ const safe = body.split("\n").filter((l) => !/x-api-key|authorization|bearer|api[-_]?key/i.test(l)).join("\n");
123
+ return safe.slice(0, max);
124
+ } catch {
125
+ return "<unreadable>";
126
+ }
127
+ }
128
+ var ClaudeHaikuContextProvider = class {
129
+ name = "claude-haiku";
130
+ apiKey;
131
+ model;
132
+ cachedDocByRelease = /* @__PURE__ */ new Map();
133
+ constructor(opts = {}) {
134
+ this.apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
135
+ this.model = opts.model ?? "claude-haiku-4-5-20251001";
136
+ if (!this.apiKey) {
137
+ throw new Error("claude-haiku context provider requires ANTHROPIC_API_KEY");
138
+ }
139
+ }
140
+ async contextFor(chunk, release) {
141
+ const releaseKey = `${release.product}@${release.version}`;
142
+ let doc = this.cachedDocByRelease.get(releaseKey);
143
+ if (!doc) {
144
+ doc = release.siblings.map((s, i) => `${i + 1}. ${s.text}`).join("\n");
145
+ this.cachedDocByRelease.set(releaseKey, doc);
146
+ }
147
+ const userPrompt = [
148
+ `Chunk: ${chunk.text}`,
149
+ ``,
150
+ `Give a 1-sentence context (max 30 words) situating this chunk in the release. Mention related products, env vars, commands. Output ONLY the context.`
151
+ ].join("\n");
152
+ const timeoutMs = providerTimeoutMs2();
153
+ let res;
154
+ try {
155
+ res = await fetch("https://api.anthropic.com/v1/messages", {
156
+ method: "POST",
157
+ headers: {
158
+ "x-api-key": this.apiKey,
159
+ "anthropic-version": "2023-06-01",
160
+ "content-type": "application/json"
161
+ },
162
+ body: JSON.stringify({
163
+ model: this.model,
164
+ max_tokens: 80,
165
+ system: [
166
+ {
167
+ type: "text",
168
+ text: `${release.product} ${release.version} (${release.releasedAt}) release notes:
169
+ ${doc}`,
170
+ cache_control: { type: "ephemeral" }
171
+ }
172
+ ],
173
+ messages: [{ role: "user", content: userPrompt }]
174
+ }),
175
+ signal: AbortSignal.timeout(timeoutMs)
176
+ });
177
+ } catch (e) {
178
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") {
179
+ throw new Error(`Anthropic API request timed out after ${timeoutMs}ms \u2014 is the API responsive?`);
180
+ }
181
+ throw e;
182
+ }
183
+ if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await safeErrorBody2(res)}`);
184
+ const json = await res.json();
185
+ return (json.content.find((c) => c.type === "text")?.text ?? "").trim();
186
+ }
187
+ };
188
+
189
+ // src/embed.ts
190
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
191
+ function initVecSchema(db) {
192
+ const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get();
193
+ if (existing) return;
194
+ const schemaPath = resolveSchemaVecPath();
195
+ const sql = readFileSync(schemaPath, "utf-8");
196
+ db.exec(sql);
197
+ }
198
+ function resolveSchemaVecPath() {
199
+ const candidates = [
200
+ join(__dirname2, "..", "schema-vec.sql"),
201
+ join(process.cwd(), "schema-vec.sql")
202
+ ];
203
+ for (const p of candidates) {
204
+ try {
205
+ readFileSync(p);
206
+ return p;
207
+ } catch {
208
+ }
209
+ }
210
+ throw new Error(`schema-vec.sql not found in: ${candidates.join(", ")}`);
211
+ }
212
+ async function embedAll(db, opts) {
213
+ initVecSchema(db);
214
+ const ctx = makeContextProvider(opts.contextProviderName);
215
+ const emb = makeEmbeddingProvider(opts.embeddingProviderName);
216
+ const productFilter = opts.product ? "AND c.product = @product" : "";
217
+ const forceFilter = opts.force ? "" : "AND ch.id IS NULL";
218
+ const limitClause = opts.limit ? "LIMIT @limit" : "";
219
+ const pendingSql = `
220
+ SELECT c.id AS change_id, c.product, c.version, r.released_at, c.kind, c.text, c.ordinal
221
+ FROM changes c
222
+ JOIN releases r ON c.product = r.product AND c.version = r.version
223
+ LEFT JOIN chunks ch ON ch.change_id = c.id
224
+ WHERE 1=1
225
+ ${productFilter}
226
+ ${forceFilter}
227
+ ORDER BY c.product, c.version, c.ordinal
228
+ ${limitClause}
229
+ `;
230
+ const params = {};
231
+ if (opts.product) params.product = opts.product;
232
+ if (opts.limit) params.limit = opts.limit;
233
+ const pending = db.prepare(pendingSql).all(params);
234
+ if (pending.length === 0) {
235
+ return {
236
+ contextProvider: ctx.name,
237
+ embeddingProvider: emb.name,
238
+ chunksCreated: 0,
239
+ chunksSkipped: 0,
240
+ contextMs: 0,
241
+ embedMs: 0,
242
+ totalMs: 0
243
+ };
244
+ }
245
+ const byRelease = /* @__PURE__ */ new Map();
246
+ for (const row of pending) {
247
+ const key = `${row.product}@${row.version}`;
248
+ if (!byRelease.has(key)) byRelease.set(key, []);
249
+ byRelease.get(key).push({
250
+ changeId: row.change_id,
251
+ product: row.product,
252
+ releaseVersion: row.version,
253
+ releasedAt: row.released_at,
254
+ kind: row.kind,
255
+ text: row.text,
256
+ ordinalInRelease: row.ordinal,
257
+ totalInRelease: 0
258
+ // backfilled below
259
+ });
260
+ }
261
+ for (const [, list] of byRelease) {
262
+ for (const c of list) c.totalInRelease = list.length;
263
+ }
264
+ const startTotal = Date.now();
265
+ let contextMs = 0;
266
+ let embedMs = 0;
267
+ const contextsByChange = /* @__PURE__ */ new Map();
268
+ const allChunks = [];
269
+ for (const [, siblings] of byRelease) {
270
+ const release = {
271
+ product: siblings[0].product,
272
+ version: siblings[0].releaseVersion,
273
+ releasedAt: siblings[0].releasedAt,
274
+ siblings
275
+ };
276
+ for (const chunk of siblings) {
277
+ if (opts.signal?.aborted) break;
278
+ const t0 = Date.now();
279
+ const ctxPrefix = await ctx.contextFor(chunk, release);
280
+ contextMs += Date.now() - t0;
281
+ contextsByChange.set(chunk.changeId, ctxPrefix);
282
+ allChunks.push(chunk);
283
+ }
284
+ if (opts.signal?.aborted) break;
285
+ }
286
+ const batchSize = opts.batchSize ?? 64;
287
+ const insertChunk = db.prepare(`
288
+ INSERT OR REPLACE INTO chunks
289
+ (change_id, product, release_version, released_at, context_prefix, original_text, contextualized, context_provider, embedding_model, embedded_at)
290
+ VALUES
291
+ (@change_id, @product, @release_version, @released_at, @context_prefix, @original_text, @contextualized, @context_provider, @embedding_model, @embedded_at)
292
+ `);
293
+ const deleteVec = db.prepare(`DELETE FROM chunks_vec WHERE rowid = ?`);
294
+ const insertVec = db.prepare(`INSERT INTO chunks_vec(rowid, embedding) VALUES (?, ?)`);
295
+ let created = 0;
296
+ let stoppedEarly = false;
297
+ let stopReason;
298
+ const usage = { requests: 0, tokens: 0 };
299
+ const totalBatches = Math.ceil(allChunks.length / batchSize);
300
+ for (let i = 0; i < allChunks.length; i += batchSize) {
301
+ if (opts.signal?.aborted) {
302
+ stoppedEarly = true;
303
+ stopReason = "cancelled";
304
+ break;
305
+ }
306
+ if (opts.maxRequests !== void 0 && usage.requests >= opts.maxRequests) {
307
+ stoppedEarly = true;
308
+ stopReason = "budget_requests";
309
+ break;
310
+ }
311
+ if (opts.maxTokens !== void 0 && usage.tokens >= opts.maxTokens) {
312
+ stoppedEarly = true;
313
+ stopReason = "budget_tokens";
314
+ break;
315
+ }
316
+ const batch = allChunks.slice(i, i + batchSize);
317
+ const texts = batch.map((c) => {
318
+ const prefix = contextsByChange.get(c.changeId) ?? "";
319
+ return prefix ? `${prefix}
320
+
321
+ ${c.text}` : c.text;
322
+ });
323
+ const t0 = Date.now();
324
+ const vectors = await emb.embed(texts);
325
+ embedMs += Date.now() - t0;
326
+ usage.requests++;
327
+ if ("usage" in emb) {
328
+ const provUsage = emb.usage;
329
+ if (provUsage && typeof provUsage.tokens === "number") {
330
+ usage.tokens = provUsage.tokens;
331
+ }
332
+ }
333
+ const tx = db.transaction(() => {
334
+ for (let j = 0; j < batch.length; j++) {
335
+ const c = batch[j];
336
+ const prefix = contextsByChange.get(c.changeId) ?? "";
337
+ const contextualized = prefix ? `${prefix}
338
+
339
+ ${c.text}` : c.text;
340
+ const result = insertChunk.run({
341
+ change_id: c.changeId,
342
+ product: c.product,
343
+ release_version: c.releaseVersion,
344
+ released_at: c.releasedAt,
345
+ context_prefix: prefix,
346
+ original_text: c.text,
347
+ contextualized,
348
+ context_provider: ctx.name,
349
+ embedding_model: `${emb.name}:${emb.model}`,
350
+ embedded_at: (/* @__PURE__ */ new Date()).toISOString()
351
+ });
352
+ const chunkId = Number(result.lastInsertRowid);
353
+ deleteVec.run(chunkId);
354
+ insertVec.run(BigInt(chunkId), vectors[j]);
355
+ created++;
356
+ }
357
+ });
358
+ tx();
359
+ if (opts.onProgress) {
360
+ opts.onProgress({
361
+ batchesCompleted: Math.floor(i / batchSize) + 1,
362
+ batchesTotal: totalBatches,
363
+ chunksCompleted: created,
364
+ chunksTotal: allChunks.length,
365
+ provider: `${emb.name}:${emb.model}`,
366
+ tokensUsed: usage.tokens,
367
+ requestsMade: usage.requests
368
+ });
369
+ }
370
+ }
371
+ const stats = {
372
+ contextProvider: ctx.name,
373
+ embeddingProvider: `${emb.name}:${emb.model}`,
374
+ chunksCreated: created,
375
+ chunksSkipped: 0,
376
+ contextMs,
377
+ embedMs,
378
+ totalMs: Date.now() - startTotal
379
+ };
380
+ if (usage.requests > 0) {
381
+ stats.usage = usage;
382
+ }
383
+ if (stoppedEarly) {
384
+ stats.stoppedEarly = true;
385
+ stats.stopReason = stopReason;
386
+ }
387
+ return stats;
388
+ }
389
+ function makeContextProvider(name) {
390
+ switch (name) {
391
+ case "none":
392
+ return new NoneContextProvider();
393
+ case "structured":
394
+ return new StructuredContextProvider();
395
+ case "ollama":
396
+ return new OllamaContextProvider();
397
+ case "claude-haiku":
398
+ return new ClaudeHaikuContextProvider();
399
+ default:
400
+ throw new Error(`unknown context provider: ${name}`);
401
+ }
402
+ }
403
+ function makeEmbeddingProvider(name) {
404
+ switch (name) {
405
+ case "ollama":
406
+ return new OllamaEmbeddingProvider();
407
+ case "voyage":
408
+ return new VoyageEmbeddingProvider();
409
+ default:
410
+ throw new Error(`unknown embedding provider: ${name}`);
411
+ }
412
+ }
413
+
414
+ // src/fetch.ts
415
+ import { execFileSync } from "child_process";
416
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
417
+ import { join as join3, resolve } from "path";
418
+
419
+ // src/fetch-rss.ts
420
+ import { XMLParser } from "fast-xml-parser";
421
+
422
+ // src/fetch-utils.ts
423
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
424
+ var GITHUB_TOKEN_HINT = "GitHub API rate limit hit. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated).";
425
+ async function fetchWithRetry(url, opts = {}) {
426
+ const {
427
+ timeoutMs = 3e4,
428
+ maxRetries = 3,
429
+ baseDelayMs = 1e3,
430
+ signal: externalSignal,
431
+ ...fetchInit
432
+ } = opts;
433
+ let lastError;
434
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
435
+ if (externalSignal?.aborted) {
436
+ throw new Error(`fetch aborted: ${externalSignal.reason?.message ?? "signal aborted"}`);
437
+ }
438
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
439
+ const combinedSignal = externalSignal ? AbortSignal.any([timeoutSignal, externalSignal]) : timeoutSignal;
440
+ try {
441
+ const res = await fetch(url, {
442
+ ...fetchInit,
443
+ signal: combinedSignal
444
+ });
445
+ if (res.ok) return res;
446
+ if (!RETRYABLE_STATUSES.has(res.status)) {
447
+ if (res.status === 403 && isGitHubApiUrl(url)) {
448
+ throw new Error(`HTTP 403 from ${url} \u2014 ${GITHUB_TOKEN_HINT}`);
449
+ }
450
+ return res;
451
+ }
452
+ if (attempt < maxRetries) {
453
+ const delay = computeDelay(res, attempt, baseDelayMs);
454
+ console.error(
455
+ `[retry] ${fetchInit.method ?? "GET"} ${String(url)} (attempt ${attempt + 2}/${maxRetries + 1}, status ${res.status}, backoff ${delay}ms)`
456
+ );
457
+ await sleep(delay, externalSignal);
458
+ continue;
459
+ }
460
+ const ghHint = res.status === 429 && isGitHubApiUrl(url) ? ` ${GITHUB_TOKEN_HINT}` : "";
461
+ throw new Error(`HTTP ${res.status} from ${url} after ${maxRetries + 1} attempts.${ghHint}`);
462
+ } catch (e) {
463
+ if (externalSignal?.aborted) {
464
+ throw new Error(`fetch aborted: ${externalSignal.reason?.message ?? "signal aborted"}`);
465
+ }
466
+ lastError = e;
467
+ if (attempt < maxRetries) {
468
+ const delay = computeDelay(null, attempt, baseDelayMs);
469
+ console.error(
470
+ `[retry] ${fetchInit.method ?? "GET"} ${String(url)} (attempt ${attempt + 2}/${maxRetries + 1}, ${e.name ?? "Error"}: ${e.message}, backoff ${delay}ms)`
471
+ );
472
+ try {
473
+ await sleep(delay, externalSignal);
474
+ } catch {
475
+ throw e;
476
+ }
477
+ continue;
478
+ }
479
+ throw e;
480
+ }
481
+ }
482
+ throw lastError ?? new Error(`fetchWithRetry: unexpected exhaustion for ${url}`);
483
+ }
484
+ function computeDelay(res, attempt, baseDelayMs) {
485
+ if (res) {
486
+ const retryAfter = res.headers.get("retry-after");
487
+ if (retryAfter) {
488
+ const seconds = Number(retryAfter);
489
+ if (!Number.isNaN(seconds) && seconds > 0) {
490
+ return Math.min(seconds * 1e3, 6e4);
491
+ }
492
+ const date = new Date(retryAfter);
493
+ if (!Number.isNaN(date.getTime())) {
494
+ const delayMs = date.getTime() - Date.now();
495
+ if (delayMs > 0) return Math.min(delayMs, 6e4);
496
+ }
497
+ }
498
+ }
499
+ const exponential = baseDelayMs * Math.pow(2, attempt);
500
+ const jitter = Math.random() * baseDelayMs * 0.5;
501
+ return Math.min(exponential + jitter, 3e4);
502
+ }
503
+ function sleep(ms, signal) {
504
+ if (signal?.aborted) return Promise.reject(new Error("sleep interrupted: signal already aborted"));
505
+ if (!signal) return new Promise((resolve3) => setTimeout(resolve3, ms));
506
+ return new Promise((resolve3, reject) => {
507
+ let settled = false;
508
+ const onAbort = () => {
509
+ if (settled) return;
510
+ settled = true;
511
+ clearTimeout(timer);
512
+ reject(new Error("sleep interrupted: signal aborted"));
513
+ };
514
+ const timer = setTimeout(() => {
515
+ if (settled) return;
516
+ settled = true;
517
+ signal.removeEventListener("abort", onAbort);
518
+ resolve3();
519
+ }, ms);
520
+ signal.addEventListener("abort", onAbort, { once: true });
521
+ });
522
+ }
523
+ function isGitHubApiUrl(url) {
524
+ try {
525
+ const hostname = typeof url === "string" ? new URL(url).hostname : url.hostname;
526
+ return hostname === "api.github.com" || hostname === "raw.githubusercontent.com";
527
+ } catch {
528
+ const s = String(url);
529
+ return s.includes("api.github.com") || s.includes("raw.githubusercontent.com");
530
+ }
531
+ }
532
+ var DEFAULT_FETCH_ALL_TIMEOUT_MS = 5 * 60 * 1e3;
533
+ function createFetchAllController(timeoutMs = DEFAULT_FETCH_ALL_TIMEOUT_MS) {
534
+ const controller = new AbortController();
535
+ const timer = setTimeout(() => {
536
+ controller.abort(new Error(`fetchAll global timeout exceeded (${timeoutMs}ms)`));
537
+ }, timeoutMs);
538
+ if (typeof timer === "object" && "unref" in timer) {
539
+ timer.unref();
540
+ }
541
+ return { controller, signal: controller.signal };
542
+ }
543
+
544
+ // src/fetch-rss.ts
545
+ async function fetchRssReleases(url, sinceIso, titleFilter, signal) {
546
+ const res = await fetchWithRetry(url, {
547
+ headers: { "user-agent": "claude-synergy/0.1.0" },
548
+ signal
549
+ });
550
+ if (!res.ok) throw new Error(`RSS ${url} returned ${res.status}`);
551
+ const xml = await res.text();
552
+ const parser = new XMLParser({
553
+ ignoreAttributes: false,
554
+ attributeNamePrefix: "@_",
555
+ cdataPropName: "__cdata",
556
+ parseTagValue: false,
557
+ trimValues: true
558
+ });
559
+ const parsed = parser.parse(xml);
560
+ const rawItems = parsed?.rss?.channel?.item ?? [];
561
+ const items = Array.isArray(rawItems) ? rawItems : [rawItems];
562
+ const out = [];
563
+ for (const it of items) {
564
+ if (!it) continue;
565
+ const title = pickString(it.title);
566
+ if (titleFilter && title && !titleFilter.test(title)) continue;
567
+ const link = pickString(it.link) ?? pickString(it.guid) ?? "";
568
+ const pubDateRaw = pickString(it.pubDate) ?? pickString(it["dc:date"]) ?? "";
569
+ const pubDateIso = parsePubDate(pubDateRaw);
570
+ if (!pubDateIso) continue;
571
+ if (pubDateIso <= sinceIso) continue;
572
+ const guid = pickString(it.guid) ?? link;
573
+ const slug = makeSlug(guid, pubDateIso, title);
574
+ const body = pickString(it["content:encoded"]) ?? pickString(it.description) ?? null;
575
+ out.push({ slug, title, link, pubDate: pubDateIso, body });
576
+ }
577
+ return out;
578
+ }
579
+ function pickString(v) {
580
+ if (v == null) return null;
581
+ if (typeof v === "string") return v.trim() || null;
582
+ if (typeof v === "object") {
583
+ const o = v;
584
+ if (typeof o.__cdata === "string") return o.__cdata.trim() || null;
585
+ if (typeof o["#text"] === "string") return o["#text"].trim() || null;
586
+ if (typeof o.toString === "function") {
587
+ const s = String(v);
588
+ if (s !== "[object Object]") return s.trim() || null;
589
+ }
590
+ }
591
+ return null;
592
+ }
593
+ function parsePubDate(raw) {
594
+ if (!raw) return null;
595
+ const d = new Date(raw);
596
+ if (Number.isNaN(d.getTime())) return null;
597
+ return d.toISOString();
598
+ }
599
+ function makeSlug(guid, isoDate, title) {
600
+ try {
601
+ const u = new URL(guid);
602
+ const parts = u.pathname.split("/").filter(Boolean);
603
+ const last = parts[parts.length - 1];
604
+ if (last && /^[a-z0-9][a-z0-9._-]*$/i.test(last)) {
605
+ return last.replace(/\.html?$/, "").toLowerCase();
606
+ }
607
+ } catch {
608
+ }
609
+ const date = isoDate.split("T")[0];
610
+ if (title) {
611
+ const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
612
+ return `${date}-${titleSlug}`;
613
+ }
614
+ return date;
615
+ }
616
+
617
+ // src/fetch-changelog.ts
618
+ import { execSync } from "child_process";
619
+ async function fetchAiderHistory(url, sinceIso, signal) {
620
+ const res = await fetchWithRetry(url, {
621
+ headers: { "user-agent": "claude-synergy/0.1.0" },
622
+ signal
623
+ });
624
+ if (!res.ok) throw new Error(`raw-changelog ${url} returned ${res.status}`);
625
+ const md = await res.text();
626
+ const items = parseAiderHistory(md);
627
+ const releaseDates = aiderReleaseDates();
628
+ for (const item of items) {
629
+ const tag = `v${item.version}`;
630
+ if (releaseDates.has(tag)) {
631
+ item.releasedAt = releaseDates.get(tag);
632
+ }
633
+ }
634
+ return items.filter((i) => !i.releasedAt || i.releasedAt > sinceIso);
635
+ }
636
+ function parseAiderHistory(md) {
637
+ const lines = md.split("\n");
638
+ const out = [];
639
+ let currentVersion = null;
640
+ let currentBody = [];
641
+ const flush = () => {
642
+ if (currentVersion) {
643
+ out.push({
644
+ version: currentVersion,
645
+ releasedAt: null,
646
+ body: currentBody.join("\n").trim()
647
+ });
648
+ }
649
+ currentBody = [];
650
+ };
651
+ for (const line of lines) {
652
+ const headerMatch = line.match(/^###\s+(?:Aider\s+)?(?:v(\d+\.\d+\.\d+(?:[-.][\w.]+)?)|main\s+branch)\s*$/i);
653
+ if (headerMatch) {
654
+ flush();
655
+ currentVersion = headerMatch[1] ?? "main";
656
+ continue;
657
+ }
658
+ if (currentVersion) currentBody.push(line);
659
+ }
660
+ flush();
661
+ return out.filter((i) => i.body.length > 0 || i.version !== "main");
662
+ }
663
+ function aiderReleaseDates() {
664
+ try {
665
+ const out = execSync(`gh api "repos/Aider-AI/aider/releases?per_page=100"`, {
666
+ encoding: "utf-8",
667
+ maxBuffer: 10 * 1024 * 1024,
668
+ stdio: ["ignore", "pipe", "pipe"]
669
+ });
670
+ const releases = JSON.parse(out);
671
+ const map = /* @__PURE__ */ new Map();
672
+ for (const r of releases) if (r.tag_name && r.published_at) map.set(r.tag_name, r.published_at);
673
+ return map;
674
+ } catch (e) {
675
+ const stderr = (e?.stderr ?? "").toString();
676
+ if (stderr.includes("403") || stderr.includes("429") || stderr.includes("rate limit")) {
677
+ console.error(
678
+ "[claude-synergy] GitHub API rate limit hit while fetching aider release dates. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated)."
679
+ );
680
+ }
681
+ return /* @__PURE__ */ new Map();
682
+ }
683
+ }
684
+
685
+ // src/fetch-html.ts
686
+ import * as cheerio from "cheerio";
687
+ var UA = { "user-agent": "claude-synergy/0.1.0" };
688
+ async function fetchGithubCopilotBlog(sinceIso, signal) {
689
+ const baseUrl = "https://github.blog/changelog/label/copilot/";
690
+ const entryUrls = /* @__PURE__ */ new Set();
691
+ for (let page = 1; page <= 3; page++) {
692
+ const url = page === 1 ? baseUrl : `${baseUrl}page/${page}/`;
693
+ try {
694
+ const res = await fetchWithRetry(url, { headers: UA, signal });
695
+ if (!res.ok) break;
696
+ const html = await res.text();
697
+ const $ = cheerio.load(html);
698
+ $('a[href*="/changelog/"]').each((_, el) => {
699
+ const href = $(el).attr("href");
700
+ if (!href) return;
701
+ const m = href.match(/^(?:https:\/\/github\.blog)?(\/changelog\/20\d{2}-\d{2}-\d{2}-[^/?#]+\/?)$/);
702
+ if (m) entryUrls.add(`https://github.blog${m[1].startsWith("/") ? m[1] : "/" + m[1]}`);
703
+ });
704
+ } catch {
705
+ break;
706
+ }
707
+ }
708
+ const items = [];
709
+ for (const url of entryUrls) {
710
+ const slugMatch = url.match(/\/changelog\/(20\d{2}-\d{2}-\d{2})-([^/]+)\/?$/);
711
+ if (!slugMatch) continue;
712
+ const dateStr = slugMatch[1];
713
+ const pubDate = (/* @__PURE__ */ new Date(`${dateStr}T12:00:00Z`)).toISOString();
714
+ if (pubDate <= sinceIso) continue;
715
+ try {
716
+ const res = await fetchWithRetry(url, { headers: UA, signal });
717
+ if (!res.ok) continue;
718
+ const html = await res.text();
719
+ const $ = cheerio.load(html);
720
+ const title = ($("h1").first().text() || $('meta[property="og:title"]').attr("content") || "").trim();
721
+ const bodyEl = $("article").first();
722
+ const body = (bodyEl.length ? bodyEl.html() : $("main").first().html()) ?? "";
723
+ items.push({
724
+ slug: `${dateStr}-${slugMatch[2]}`,
725
+ title,
726
+ pubDate,
727
+ link: url,
728
+ body: body.trim()
729
+ });
730
+ } catch {
731
+ continue;
732
+ }
733
+ }
734
+ return items;
735
+ }
736
+ async function fetchWindsurfChangelog(sinceIso, signal) {
737
+ const url = "https://windsurf.com/changelog";
738
+ const res = await fetchWithRetry(url, { headers: UA, signal });
739
+ if (!res.ok) throw new Error(`Windsurf ${res.status}`);
740
+ const html = await res.text();
741
+ const $ = cheerio.load(html);
742
+ const nextDataScript = $("script#__NEXT_DATA__").html();
743
+ if (!nextDataScript) {
744
+ 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.");
745
+ return [];
746
+ }
747
+ try {
748
+ const data = JSON.parse(nextDataScript);
749
+ const entries = findChangelogEntries(data);
750
+ if (!entries || entries.length === 0) {
751
+ console.error("[fetch-html] windsurf: __NEXT_DATA__ present but no entry array found. 0 entries.");
752
+ return [];
753
+ }
754
+ const items = [];
755
+ for (const e of entries) {
756
+ const title = (e.title ?? e.heading ?? e.name ?? "").toString().trim();
757
+ const dateRaw = (e.date ?? e.publishedAt ?? e.published_at ?? "").toString();
758
+ if (!title || !dateRaw) continue;
759
+ const d = new Date(dateRaw);
760
+ if (Number.isNaN(d.getTime())) continue;
761
+ const pubDate = d.toISOString();
762
+ if (pubDate <= sinceIso) continue;
763
+ const body = (e.body ?? e.content ?? e.description ?? "").toString();
764
+ const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
765
+ const slug = `${pubDate.split("T")[0]}-${titleSlug || "entry"}`;
766
+ items.push({ slug, title, pubDate, link: `${url}#${titleSlug}`, body });
767
+ }
768
+ return items;
769
+ } catch (e) {
770
+ console.error(`[fetch-html] windsurf: __NEXT_DATA__ parse failed: ${e.message}`);
771
+ return [];
772
+ }
773
+ }
774
+ function findChangelogEntries(obj, depth = 0) {
775
+ if (depth > 8 || !obj || typeof obj !== "object") return null;
776
+ if (Array.isArray(obj)) {
777
+ 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")) {
778
+ return obj;
779
+ }
780
+ return null;
781
+ }
782
+ for (const v of Object.values(obj)) {
783
+ const found = findChangelogEntries(v, depth + 1);
784
+ if (found) return found;
785
+ }
786
+ return null;
787
+ }
788
+ var VSCODE_V_TO_MONTH = {
789
+ // version → [year, month-1 (0-indexed)]
790
+ 100: [2025, 3],
791
+ // April 2025
792
+ 101: [2025, 4],
793
+ // May 2025
794
+ 102: [2025, 5],
795
+ // June 2025
796
+ 103: [2025, 6],
797
+ // July 2025
798
+ 104: [2025, 7],
799
+ // August 2025
800
+ 105: [2025, 8],
801
+ // September 2025
802
+ 106: [2025, 9],
803
+ // October 2025
804
+ 107: [2025, 10],
805
+ // November 2025
806
+ 108: [2025, 11],
807
+ // December 2025
808
+ 109: [2026, 0],
809
+ // January 2026
810
+ 110: [2026, 1],
811
+ // February 2026
812
+ 111: [2026, 2],
813
+ // March 2026
814
+ 112: [2026, 3],
815
+ // April 2026
816
+ 113: [2026, 4],
817
+ // May 2026
818
+ 114: [2026, 5],
819
+ // June 2026 (future / Insiders)
820
+ 115: [2026, 6],
821
+ // July 2026 (future / Insiders)
822
+ 116: [2026, 7],
823
+ 117: [2026, 8],
824
+ 118: [2026, 9],
825
+ 119: [2026, 10],
826
+ 120: [2026, 11]
827
+ };
828
+ function parseVscodeReleaseDate(html, $) {
829
+ const releaseDateMatch = html.match(
830
+ /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
831
+ );
832
+ if (releaseDateMatch) {
833
+ const d = new Date(releaseDateMatch[1]);
834
+ if (!Number.isNaN(d.getTime())) return d.toISOString();
835
+ }
836
+ const timeEl = $("time[datetime]").first();
837
+ if (timeEl.length) {
838
+ const iso = timeEl.attr("datetime");
839
+ if (iso) {
840
+ const d = new Date(iso);
841
+ if (!Number.isNaN(d.getTime())) return d.toISOString();
842
+ }
843
+ }
844
+ const welcomeMatch = html.match(
845
+ /Welcome to the\s+((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{4})/i
846
+ );
847
+ if (welcomeMatch) {
848
+ const d = /* @__PURE__ */ new Date(`${welcomeMatch[1]} 15`);
849
+ if (!Number.isNaN(d.getTime())) return d.toISOString();
850
+ }
851
+ return null;
852
+ }
853
+ async function fetchVscodeUpdates(sinceIso, signal) {
854
+ const items = [];
855
+ for (const [vStr, [year, monthIdx]] of Object.entries(VSCODE_V_TO_MONTH)) {
856
+ const v = parseInt(vStr, 10);
857
+ const fallbackIsoDate = new Date(Date.UTC(year, monthIdx, 15, 12)).toISOString();
858
+ const versionTag = `v1_${v}`;
859
+ const url = `https://code.visualstudio.com/updates/${versionTag}`;
860
+ const fallbackPlus31d = new Date(Date.parse(fallbackIsoDate) + 31 * 24 * 3600 * 1e3).toISOString();
861
+ if (fallbackPlus31d <= sinceIso) continue;
862
+ try {
863
+ const res = await fetchWithRetry(url, { headers: UA, signal, maxRetries: 1 });
864
+ if (res.status === 404) continue;
865
+ if (!res.ok) continue;
866
+ const html = await res.text();
867
+ const $ = cheerio.load(html);
868
+ let isoDate = parseVscodeReleaseDate(html, $);
869
+ if (!isoDate) {
870
+ console.warn(
871
+ `[fetch-html] vscode ${versionTag}: no Release-date line found; falling back to mid-month (${fallbackIsoDate.split("T")[0]})`
872
+ );
873
+ isoDate = fallbackIsoDate;
874
+ }
875
+ if (isoDate <= sinceIso) continue;
876
+ const title = $("h1").first().text().trim() || `Visual Studio Code 1.${v}`;
877
+ const body = ($("main").first().html() || $("article").first().html() || "").trim();
878
+ if (body.length < 200) continue;
879
+ items.push({
880
+ slug: versionTag,
881
+ title,
882
+ pubDate: isoDate,
883
+ link: url,
884
+ body
885
+ });
886
+ } catch {
887
+ continue;
888
+ }
889
+ }
890
+ return items;
891
+ }
892
+ async function fetchHtmlReleases(parser, sinceIso, signal) {
893
+ switch (parser) {
894
+ case "github-copilot-blog":
895
+ return fetchGithubCopilotBlog(sinceIso, signal);
896
+ case "windsurf-changelog":
897
+ return fetchWindsurfChangelog(sinceIso, signal);
898
+ case "vscode-updates":
899
+ return fetchVscodeUpdates(sinceIso, signal);
900
+ }
901
+ }
902
+
903
+ // src/fetch-mcp-registry.ts
904
+ import { writeFileSync, mkdirSync } from "fs";
905
+ import { join as join2 } from "path";
906
+ var UA2 = { "user-agent": "claude-synergy/0.1.0", accept: "application/json" };
907
+ async function fetchOfficialMcpRegistry(opts = {}) {
908
+ const maxPages = opts.maxPages ?? 50;
909
+ const limit = 100;
910
+ let cursor;
911
+ const byName = /* @__PURE__ */ new Map();
912
+ for (let page = 0; page < maxPages; page++) {
913
+ const url = `https://registry.modelcontextprotocol.io/v0/servers?limit=${limit}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
914
+ const res = await fetchWithRetry(url, { headers: UA2, signal: opts.signal });
915
+ if (!res.ok) throw new Error(`Official MCP Registry ${res.status}`);
916
+ const json = await res.json();
917
+ if (!Array.isArray(json.servers)) break;
918
+ for (const item of json.servers) {
919
+ const s = item.server;
920
+ if (!s?.name) continue;
921
+ const meta = item._meta?.["io.modelcontextprotocol.registry/official"];
922
+ const isLatest = meta?.isLatest === true;
923
+ const existing = byName.get(s.name);
924
+ if (existing && !isLatest) continue;
925
+ byName.set(s.name, {
926
+ slug: slugify(s.name),
927
+ name: s.title ?? s.name,
928
+ description: (s.description ?? "").trim(),
929
+ homepage: s.remotes?.[0]?.url ?? null,
930
+ popularity: null,
931
+ createdAt: meta?.publishedAt ?? null,
932
+ source: "official-mcp-registry",
933
+ category: null,
934
+ verified: null
935
+ });
936
+ }
937
+ const next = json.metadata?.nextCursor;
938
+ if (!next || next === cursor) break;
939
+ cursor = next;
940
+ }
941
+ return [...byName.values()];
942
+ }
943
+ async function fetchSmitheryRegistry(opts = {}) {
944
+ const pageSize = opts.pageSize ?? 100;
945
+ const maxEntries = opts.maxEntries ?? 200;
946
+ const out = [];
947
+ let page = 1;
948
+ while (out.length < maxEntries) {
949
+ const url = `https://registry.smithery.ai/servers?page=${page}&pageSize=${pageSize}`;
950
+ const res = await fetchWithRetry(url, { headers: UA2, signal: opts.signal });
951
+ if (!res.ok) throw new Error(`Smithery Registry ${res.status}`);
952
+ const json = await res.json();
953
+ if (!Array.isArray(json.servers) || json.servers.length === 0) break;
954
+ for (const s of json.servers) {
955
+ if (out.length >= maxEntries) break;
956
+ out.push({
957
+ slug: slugify(s.qualifiedName ?? s.namespace ?? s.id),
958
+ name: s.displayName ?? s.qualifiedName,
959
+ description: (s.description ?? "").trim(),
960
+ homepage: s.homepage ?? null,
961
+ popularity: typeof s.useCount === "number" ? s.useCount : null,
962
+ createdAt: s.createdAt ?? null,
963
+ source: "smithery",
964
+ category: null,
965
+ verified: s.verified ?? null
966
+ });
967
+ }
968
+ const total = json.pagination?.totalPages ?? 1;
969
+ if (page >= total) break;
970
+ page++;
971
+ }
972
+ out.sort((a, b) => (b.popularity ?? 0) - (a.popularity ?? 0));
973
+ return out;
974
+ }
975
+ function writeCatalog(productDir, productName, entries) {
976
+ mkdirSync(productDir, { recursive: true });
977
+ const catalogPath = join2(productDir, "CATALOG.md");
978
+ const lines = [];
979
+ lines.push(`# ${productName} \u2014 MCP server catalog`);
980
+ lines.push("");
981
+ lines.push(`**Source:** ${entries[0]?.source ?? "unknown"}`);
982
+ lines.push(`**Entries:** ${entries.length}`);
983
+ lines.push(`**Fetched:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
984
+ lines.push("");
985
+ lines.push("| Name | Description | Popularity | Created | Homepage |");
986
+ lines.push("|------|-------------|------------|---------|----------|");
987
+ for (const e of entries) {
988
+ const desc = (e.description.split("\n")[0] ?? "").slice(0, 160).replace(/\|/g, "\\|");
989
+ const pop = e.popularity != null ? String(e.popularity) : "\u2014";
990
+ const date = e.createdAt ? e.createdAt.split("T")[0] : "\u2014";
991
+ const home = e.homepage ? `[link](${e.homepage})` : "\u2014";
992
+ const name = e.name.replace(/\|/g, "\\|");
993
+ lines.push(`| ${name} | ${desc} | ${pop} | ${date} | ${home} |`);
994
+ }
995
+ lines.push("");
996
+ writeFileSync(catalogPath, lines.join("\n"), "utf-8");
997
+ return {
998
+ source: entries[0]?.source ?? "unknown",
999
+ entriesFetched: entries.length,
1000
+ productDir,
1001
+ catalogPath
1002
+ };
1003
+ }
1004
+ function slugify(s) {
1005
+ return s.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
1006
+ }
1007
+ async function fetchCatalog(catalogType, opts = {}) {
1008
+ switch (catalogType) {
1009
+ case "official-mcp-registry":
1010
+ return fetchOfficialMcpRegistry({ maxPages: opts.maxPages, signal: opts.signal });
1011
+ case "smithery":
1012
+ return fetchSmitheryRegistry({ maxEntries: opts.maxEntries, signal: opts.signal });
1013
+ }
1014
+ }
1015
+
1016
+ // src/fetch.ts
1017
+ var REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
1018
+ var PRODUCT_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1019
+ function assertRepoShape(repo) {
1020
+ if (!REPO_PATTERN.test(repo)) {
1021
+ throw new Error(
1022
+ `invalid repo: ${JSON.stringify(repo)} \u2014 expected "<org>/<repo>" with safe chars [A-Za-z0-9._-]`
1023
+ );
1024
+ }
1025
+ }
1026
+ function assertProductShape(product) {
1027
+ if (!PRODUCT_PATTERN.test(product)) {
1028
+ throw new Error(
1029
+ `invalid product name: ${JSON.stringify(product)} \u2014 expected [a-z0-9][a-z0-9-]*`
1030
+ );
1031
+ }
1032
+ }
1033
+ function sanitizeFilename(s) {
1034
+ const cleaned = s.replace(/[\/\\]/g, "-").replace(/^\.+/, "").replace(/\.\./g, "-").replace(/[<>:"|?*\x00-\x1f]/g, "-").slice(0, 100);
1035
+ if (cleaned.includes("..") || cleaned.includes("/") || cleaned.includes("\\")) {
1036
+ throw new Error(`sanitizeFilename produced unsafe output: ${JSON.stringify(cleaned)}`);
1037
+ }
1038
+ return cleaned;
1039
+ }
1040
+ function assertSafeFilename(name, context) {
1041
+ if (!name || name.includes("..") || name.includes("/") || name.includes("\\")) {
1042
+ throw new Error(`unsafe filename in ${context}: ${JSON.stringify(name)}`);
1043
+ }
1044
+ }
1045
+ var HARDCODED_FALLBACK_TARGETS = [
1046
+ // ── Existing Anthropic GH-Releases sources ────────────────────────────────
1047
+ { product: "claude-agent-sdk-python", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-python" },
1048
+ { product: "claude-agent-sdk-typescript", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-typescript" },
1049
+ { product: "anthropic-cli", strategy: "gh-releases", repo: "anthropics/anthropic-cli" },
1050
+ { product: "anthropic-sdk-python", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-python" },
1051
+ { product: "anthropic-sdk-typescript", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-typescript", multiPackage: true },
1052
+ { product: "anthropic-sdk-go", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-go" },
1053
+ { product: "anthropic-sdk-java", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-java" },
1054
+ { product: "anthropic-sdk-ruby", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-ruby" },
1055
+ { product: "anthropic-sdk-csharp", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-csharp", multiPackage: true },
1056
+ { product: "anthropic-sdk-php", strategy: "gh-releases", repo: "anthropics/anthropic-sdk-php" },
1057
+ { product: "claude-code-action", strategy: "gh-releases", repo: "anthropics/claude-code-action" },
1058
+ { product: "claude-code-security-review", strategy: "gh-releases", repo: "anthropics/claude-code-security-review" },
1059
+ // ── Tier 4a additions ─────────────────────────────────────────────────────
1060
+ // MCP ecosystem SDKs (modelcontextprotocol/*)
1061
+ { product: "mcp-python-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/python-sdk" },
1062
+ { product: "mcp-typescript-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/typescript-sdk", multiPackage: true },
1063
+ { product: "mcp-go-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/go-sdk" },
1064
+ { product: "mcp-java-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/java-sdk" },
1065
+ { product: "mcp-csharp-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/csharp-sdk" },
1066
+ { product: "mcp-kotlin-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/kotlin-sdk" },
1067
+ { product: "mcp-ruby-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/ruby-sdk" },
1068
+ { product: "mcp-swift-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/swift-sdk" },
1069
+ { product: "mcp-rust-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/rust-sdk" },
1070
+ { product: "mcp-php-sdk", strategy: "gh-releases", repo: "modelcontextprotocol/php-sdk" },
1071
+ { product: "mcp-spec", strategy: "gh-releases", repo: "modelcontextprotocol/modelcontextprotocol" },
1072
+ { product: "mcp-inspector", strategy: "gh-releases", repo: "modelcontextprotocol/inspector" },
1073
+ { product: "mcp-registry", strategy: "gh-releases", repo: "modelcontextprotocol/registry" },
1074
+ { product: "mcp-mcpb", strategy: "gh-releases", repo: "modelcontextprotocol/mcpb" },
1075
+ { product: "mcp-conformance", strategy: "gh-releases", repo: "modelcontextprotocol/conformance" },
1076
+ // Continue.dev (multi-platform tagged releases)
1077
+ { product: "continue-dev", strategy: "gh-releases", repo: "continuedev/continue", multiPackage: true },
1078
+ { product: "continue-cli", strategy: "gh-releases", repo: "continuedev/continue-cli" },
1079
+ // RSS-based feeds
1080
+ { product: "cursor", strategy: "rss", rssUrl: "https://cursor.com/changelog/rss.xml" },
1081
+ { product: "cody-enterprise", strategy: "rss", rssUrl: "https://sourcegraph.com/changelog/featured.rss", rssTitleFilter: /cody|sourcegraph/i },
1082
+ // Raw markdown CHANGELOG
1083
+ { product: "aider", strategy: "raw-changelog", rawChangelogUrl: "https://raw.githubusercontent.com/Aider-AI/aider/main/HISTORY.md", rawChangelogParser: "aider-history" },
1084
+ // ── Tier 4b additions — HTML-scraped sources ──────────────────────────────
1085
+ { product: "github-copilot", strategy: "html-scrape", htmlParser: "github-copilot-blog" },
1086
+ { product: "vscode-copilot-chat", strategy: "html-scrape", htmlParser: "vscode-updates" },
1087
+ { product: "windsurf", strategy: "html-scrape", htmlParser: "windsurf-changelog" }
1088
+ ];
1089
+ var TARGETS = loadProductsConfig()?.fetchTargets ?? HARDCODED_FALLBACK_TARGETS;
1090
+ async function fetchAll(db, productsRoot, opts = {}) {
1091
+ const targets = opts.product ? TARGETS.filter((t) => t.product === opts.product) : TARGETS;
1092
+ if (targets.length === 0) {
1093
+ throw new Error(`unknown product: ${opts.product} (available: ${TARGETS.map((t) => t.product).join(", ")})`);
1094
+ }
1095
+ const total = targets.length;
1096
+ const onProgress = opts.onProgress;
1097
+ const globalTimeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_ALL_TIMEOUT_MS;
1098
+ const { controller, signal } = createFetchAllController(globalTimeoutMs);
1099
+ const results = [];
1100
+ try {
1101
+ for (let i = 0; i < targets.length; i++) {
1102
+ const target = targets[i];
1103
+ if (signal.aborted) {
1104
+ onProgress?.({ type: "skip", product: target.product, index: i, total, error: "global timeout exceeded" });
1105
+ results.push({
1106
+ product: target.product,
1107
+ fetched: 0,
1108
+ newSince: opts.sinceOverride ?? "",
1109
+ latest: null,
1110
+ errors: ["skipped: global timeout exceeded"]
1111
+ });
1112
+ continue;
1113
+ }
1114
+ onProgress?.({ type: "start", product: target.product, index: i, total });
1115
+ const stats = await fetchOne(db, productsRoot, target, opts.sinceOverride, signal);
1116
+ results.push(stats);
1117
+ if (stats.errors.length > 0) {
1118
+ onProgress?.({ type: "error", product: target.product, index: i, total, error: stats.errors[0] });
1119
+ } else {
1120
+ onProgress?.({ type: "done", product: target.product, index: i, total });
1121
+ }
1122
+ }
1123
+ } finally {
1124
+ controller.abort();
1125
+ }
1126
+ const augmented = results;
1127
+ augmented.summary = buildSummary(results);
1128
+ return augmented;
1129
+ }
1130
+ function buildSummary(results) {
1131
+ let succeeded = 0;
1132
+ let failed = 0;
1133
+ let skipped = 0;
1134
+ let newChanges = 0;
1135
+ const errors = [];
1136
+ for (const r of results) {
1137
+ const isSkipped = r.errors.some((e) => e.startsWith("skipped:"));
1138
+ const isFailed = r.errors.length > 0 && !isSkipped;
1139
+ if (isSkipped) {
1140
+ skipped++;
1141
+ } else if (isFailed) {
1142
+ failed++;
1143
+ for (const e of r.errors) {
1144
+ errors.push({ product: r.product, error: e });
1145
+ }
1146
+ } else {
1147
+ succeeded++;
1148
+ }
1149
+ newChanges += r.fetched;
1150
+ }
1151
+ return { total: results.length, succeeded, failed, skipped, newChanges, errors };
1152
+ }
1153
+ async function fetchOne(db, productsRoot, target, sinceOverride, signal) {
1154
+ assertProductShape(target.product);
1155
+ const since = sinceOverride ?? readMarker(db, target.product) ?? "2026-01-01";
1156
+ const outDir = resolve(productsRoot, target.product, "releases");
1157
+ mkdirSync2(outDir, { recursive: true });
1158
+ try {
1159
+ switch (target.strategy) {
1160
+ case "gh-releases":
1161
+ return await fetchGhReleases(db, outDir, target, since);
1162
+ case "rss":
1163
+ return await fetchRss(db, outDir, target, since, signal);
1164
+ case "raw-changelog":
1165
+ return await fetchRawChangelog(db, outDir, target, since, signal);
1166
+ case "html-scrape":
1167
+ return await fetchHtmlScrape(db, outDir, target, since, signal);
1168
+ case "catalog":
1169
+ return await fetchCatalogStrategy(db, productsRoot, target, since, signal);
1170
+ case "playwright":
1171
+ return await fetchPlaywrightStrategy(db, outDir, target, since);
1172
+ }
1173
+ } catch (e) {
1174
+ return {
1175
+ product: target.product,
1176
+ fetched: 0,
1177
+ newSince: since,
1178
+ latest: null,
1179
+ errors: [`fetch failed: ${e.message}`]
1180
+ };
1181
+ }
1182
+ }
1183
+ async function fetchGhReleases(db, outDir, target, since) {
1184
+ if (!target.repo) throw new Error(`${target.product}: gh-releases strategy requires repo`);
1185
+ const releases = ghReleases(target.repo, since);
1186
+ let latest = null;
1187
+ let fetched = 0;
1188
+ const errors = [];
1189
+ for (const r of releases) {
1190
+ try {
1191
+ const baseName = filenameFor(target, r.tag_name);
1192
+ const filename = sanitizeFilename(baseName);
1193
+ assertSafeFilename(filename, `gh-releases filename for tag ${JSON.stringify(r.tag_name)}`);
1194
+ const vName = sanitizeFilename(`v${baseName}`);
1195
+ assertSafeFilename(vName, `gh-releases vName for tag ${JSON.stringify(r.tag_name)}`);
1196
+ const path = join3(outDir, `${filename}.md`);
1197
+ const vPath = join3(outDir, `${vName}.md`);
1198
+ if (existsSync(path) || existsSync(vPath)) {
1199
+ if (!latest || r.published_at > latest) latest = r.published_at;
1200
+ continue;
1201
+ }
1202
+ writeFileSync2(path, renderGhRelease(target.product, r), "utf-8");
1203
+ fetched++;
1204
+ if (!latest || r.published_at > latest) latest = r.published_at;
1205
+ } catch (e) {
1206
+ errors.push(`${r.tag_name}: ${e.message}`);
1207
+ }
1208
+ }
1209
+ if (latest) writeMarker(db, target.product, latest, target.strategy);
1210
+ return { product: target.product, fetched, newSince: since, latest, errors };
1211
+ }
1212
+ function ghReleases(repo, sinceIso) {
1213
+ assertRepoShape(repo);
1214
+ const all = [];
1215
+ const MAX_PAGES = 50;
1216
+ for (let page = 1; page <= MAX_PAGES; page++) {
1217
+ let out;
1218
+ try {
1219
+ out = execFileSync(
1220
+ "gh",
1221
+ ["api", `repos/${repo}/releases?per_page=100&page=${page}`],
1222
+ { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }
1223
+ );
1224
+ } catch (e) {
1225
+ const stderr = (e.stderr ?? "").toString();
1226
+ if (stderr.includes("404")) return page === 1 ? [] : all;
1227
+ if (stderr.includes("403") || stderr.includes("429") || stderr.includes("rate limit")) {
1228
+ throw new Error(
1229
+ `GitHub API rate limit hit for ${repo}. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated). Original error: ${e.message}`
1230
+ );
1231
+ }
1232
+ throw e;
1233
+ }
1234
+ let batch;
1235
+ try {
1236
+ batch = JSON.parse(out);
1237
+ } catch {
1238
+ return page === 1 ? [] : all;
1239
+ }
1240
+ if (!Array.isArray(batch) || batch.length === 0) break;
1241
+ all.push(...batch);
1242
+ if (batch.length < 100) break;
1243
+ }
1244
+ return all.filter((r) => r.published_at && r.published_at > sinceIso).map((r) => ({
1245
+ tag_name: r.tag_name,
1246
+ published_at: r.published_at,
1247
+ name: r.name,
1248
+ body: r.body,
1249
+ html_url: r.html_url
1250
+ }));
1251
+ }
1252
+ function filenameFor(target, tag) {
1253
+ if (target.multiPackage) {
1254
+ return tag.replace(/^@/, "").replace(/\//g, "-").replace(/@/g, "-").replace(/-v(\d)/g, "-$1").replace(/^v/, "");
1255
+ }
1256
+ return tag.replace(/^v/, "");
1257
+ }
1258
+ function yamlQuote(s) {
1259
+ if (s == null) return "";
1260
+ return s.replace(/[\x00-\x1f]/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1261
+ }
1262
+ function renderGhRelease(product, r) {
1263
+ const version = (r.tag_name ?? "").replace(/^v/, "");
1264
+ return [
1265
+ "---",
1266
+ `product: ${product}`,
1267
+ `version: "${yamlQuote(version)}"`,
1268
+ `released_at: "${r.published_at.split("T")[0]}"`,
1269
+ `source_url: "${yamlQuote(r.html_url)}"`,
1270
+ `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1271
+ "---",
1272
+ "",
1273
+ `# ${product} ${r.tag_name}`,
1274
+ "",
1275
+ r.body ?? "(no body)",
1276
+ ""
1277
+ ].join("\n");
1278
+ }
1279
+ async function fetchRss(db, outDir, target, since, signal) {
1280
+ if (!target.rssUrl) throw new Error(`${target.product}: rss strategy requires rssUrl`);
1281
+ const items = await fetchRssReleases(target.rssUrl, since, target.rssTitleFilter, signal);
1282
+ let latest = null;
1283
+ let fetched = 0;
1284
+ const errors = [];
1285
+ for (const item of items) {
1286
+ try {
1287
+ const safeSlug = sanitizeFilename(item.slug);
1288
+ assertSafeFilename(safeSlug, `rss slug for ${JSON.stringify(item.slug)}`);
1289
+ const path = join3(outDir, `${safeSlug}.md`);
1290
+ if (existsSync(path)) {
1291
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1292
+ continue;
1293
+ }
1294
+ const body = [
1295
+ "---",
1296
+ `product: ${target.product}`,
1297
+ `version: "${yamlQuote(safeSlug)}"`,
1298
+ `released_at: "${item.pubDate.split("T")[0]}"`,
1299
+ `source_url: "${yamlQuote(item.link)}"`,
1300
+ `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1301
+ `title: "${yamlQuote(item.title ?? "")}"`,
1302
+ "---",
1303
+ "",
1304
+ `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
1305
+ "",
1306
+ item.body ?? "(no body)",
1307
+ ""
1308
+ ].join("\n");
1309
+ writeFileSync2(path, body, "utf-8");
1310
+ fetched++;
1311
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1312
+ } catch (e) {
1313
+ errors.push(`${item.slug}: ${e.message}`);
1314
+ }
1315
+ }
1316
+ if (latest) writeMarker(db, target.product, latest, target.strategy);
1317
+ return { product: target.product, fetched, newSince: since, latest, errors };
1318
+ }
1319
+ async function fetchRawChangelog(db, outDir, target, since, signal) {
1320
+ if (!target.rawChangelogUrl) throw new Error(`${target.product}: raw-changelog requires url`);
1321
+ if (target.rawChangelogParser !== "aider-history") {
1322
+ throw new Error(`${target.product}: unsupported parser ${target.rawChangelogParser}`);
1323
+ }
1324
+ const items = await fetchAiderHistory(target.rawChangelogUrl, since, signal);
1325
+ let latest = null;
1326
+ let fetched = 0;
1327
+ const errors = [];
1328
+ for (const item of items) {
1329
+ try {
1330
+ const safeVersion = sanitizeFilename(item.version);
1331
+ assertSafeFilename(safeVersion, `raw-changelog version for ${JSON.stringify(item.version)}`);
1332
+ const path = join3(outDir, `${safeVersion}.md`);
1333
+ if (existsSync(path)) {
1334
+ if (item.releasedAt && (!latest || item.releasedAt > latest)) latest = item.releasedAt;
1335
+ continue;
1336
+ }
1337
+ const body = [
1338
+ "---",
1339
+ `product: ${target.product}`,
1340
+ `version: "${yamlQuote(safeVersion)}"`,
1341
+ `released_at: ${item.releasedAt ? `"${yamlQuote(item.releasedAt)}"` : "null"}`,
1342
+ `source_url: "${yamlQuote(target.rawChangelogUrl ?? "")}"`,
1343
+ `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1344
+ "---",
1345
+ "",
1346
+ `# ${target.product} v${item.version}`,
1347
+ "",
1348
+ item.body,
1349
+ ""
1350
+ ].join("\n");
1351
+ writeFileSync2(path, body, "utf-8");
1352
+ fetched++;
1353
+ if (item.releasedAt && (!latest || item.releasedAt > latest)) latest = item.releasedAt;
1354
+ } catch (e) {
1355
+ errors.push(`${item.version}: ${e.message}`);
1356
+ }
1357
+ }
1358
+ if (latest) writeMarker(db, target.product, latest, target.strategy);
1359
+ return { product: target.product, fetched, newSince: since, latest, errors };
1360
+ }
1361
+ async function fetchHtmlScrape(db, outDir, target, since, signal) {
1362
+ if (!target.htmlParser) throw new Error(`${target.product}: html-scrape requires htmlParser`);
1363
+ const items = await fetchHtmlReleases(target.htmlParser, since, signal);
1364
+ let latest = null;
1365
+ let fetched = 0;
1366
+ const errors = [];
1367
+ for (const item of items) {
1368
+ try {
1369
+ const safeSlug = sanitizeFilename(item.slug);
1370
+ assertSafeFilename(safeSlug, `html-scrape slug for ${JSON.stringify(item.slug)}`);
1371
+ const path = join3(outDir, `${safeSlug}.md`);
1372
+ if (existsSync(path)) {
1373
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1374
+ continue;
1375
+ }
1376
+ const body = [
1377
+ "---",
1378
+ `product: ${target.product}`,
1379
+ `version: "${yamlQuote(safeSlug)}"`,
1380
+ `released_at: "${item.pubDate.split("T")[0]}"`,
1381
+ `source_url: "${yamlQuote(item.link)}"`,
1382
+ `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1383
+ `title: "${yamlQuote(item.title ?? "")}"`,
1384
+ "---",
1385
+ "",
1386
+ `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
1387
+ "",
1388
+ item.body ?? "(no body)",
1389
+ ""
1390
+ ].join("\n");
1391
+ writeFileSync2(path, body, "utf-8");
1392
+ fetched++;
1393
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1394
+ } catch (e) {
1395
+ errors.push(`${item.slug}: ${e.message}`);
1396
+ }
1397
+ }
1398
+ if (latest) writeMarker(db, target.product, latest, target.strategy);
1399
+ return { product: target.product, fetched, newSince: since, latest, errors };
1400
+ }
1401
+ async function fetchPlaywrightStrategy(db, outDir, target, since) {
1402
+ const { fetchWindsurfWithPlaywright } = await import("./fetch-playwright-HQ6OTMSQ.js");
1403
+ const items = await fetchWindsurfWithPlaywright(since);
1404
+ let latest = null;
1405
+ let fetched = 0;
1406
+ const errors = [];
1407
+ for (const item of items) {
1408
+ try {
1409
+ const safeSlug = sanitizeFilename(item.slug);
1410
+ assertSafeFilename(safeSlug, `playwright slug for ${JSON.stringify(item.slug)}`);
1411
+ const path = join3(outDir, `${safeSlug}.md`);
1412
+ if (existsSync(path)) {
1413
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1414
+ continue;
1415
+ }
1416
+ const body = [
1417
+ "---",
1418
+ `product: ${target.product}`,
1419
+ `version: "${yamlQuote(safeSlug)}"`,
1420
+ `released_at: "${item.pubDate.split("T")[0]}"`,
1421
+ `source_url: "${yamlQuote(item.link)}"`,
1422
+ `fetched_at: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"`,
1423
+ `title: "${yamlQuote(item.title ?? "")}"`,
1424
+ "---",
1425
+ "",
1426
+ `# ${target.product} \u2014 ${item.title ?? safeSlug}`,
1427
+ "",
1428
+ item.body ?? "(no body)",
1429
+ ""
1430
+ ].join("\n");
1431
+ writeFileSync2(path, body, "utf-8");
1432
+ fetched++;
1433
+ if (!latest || item.pubDate > latest) latest = item.pubDate;
1434
+ } catch (e) {
1435
+ errors.push(`${item.slug}: ${e.message}`);
1436
+ }
1437
+ }
1438
+ if (latest) writeMarker(db, target.product, latest, target.strategy);
1439
+ return { product: target.product, fetched, newSince: since, latest, errors };
1440
+ }
1441
+ async function fetchCatalogStrategy(db, productsRoot, target, since, signal) {
1442
+ if (!target.catalogType) throw new Error(`${target.product}: catalog requires catalogType`);
1443
+ const entries = await fetchCatalog(target.catalogType, { maxEntries: target.catalogMaxEntries, signal });
1444
+ const productDir = resolve(productsRoot, target.product);
1445
+ writeCatalog(productDir, target.product, entries);
1446
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1447
+ writeMarker(db, target.product, nowIso, target.strategy);
1448
+ return {
1449
+ product: target.product,
1450
+ fetched: entries.length,
1451
+ newSince: since,
1452
+ latest: nowIso,
1453
+ errors: []
1454
+ };
1455
+ }
1456
+ function readMarker(db, product) {
1457
+ const row = db.prepare(`SELECT version FROM markers WHERE product = ? AND name = 'last_fetched_release_at'`).get(product);
1458
+ return row?.version ?? null;
1459
+ }
1460
+ function writeMarker(db, product, isoTimestamp, fetchStrategy = "gh-releases") {
1461
+ assertProductShape(product);
1462
+ db.prepare(`
1463
+ INSERT OR IGNORE INTO products (name, display_name, source_tier, source_url, fetch_strategy, notes)
1464
+ VALUES (?, ?, 1, '', ?, NULL)
1465
+ `).run(product, product, fetchStrategy);
1466
+ db.prepare(`
1467
+ INSERT INTO markers (product, name, version, updated_at)
1468
+ VALUES (?, 'last_fetched_release_at', ?, ?)
1469
+ ON CONFLICT(product, name) DO UPDATE SET version = excluded.version, updated_at = excluded.updated_at
1470
+ `).run(product, isoTimestamp, (/* @__PURE__ */ new Date()).toISOString());
1471
+ }
1472
+ function seedMarkersFromDb(db) {
1473
+ const rows = db.prepare(
1474
+ `SELECT product, MAX(released_at) AS max_date FROM releases WHERE released_at IS NOT NULL GROUP BY product`
1475
+ ).all();
1476
+ const out = [];
1477
+ for (const t of TARGETS) {
1478
+ const r = rows.find((x) => x.product === t.product);
1479
+ if (r?.max_date) {
1480
+ const iso = r.max_date.includes("T") ? r.max_date : `${r.max_date}T23:59:59Z`;
1481
+ writeMarker(db, t.product, iso, t.strategy);
1482
+ out.push({ product: t.product, seededTo: iso });
1483
+ } else {
1484
+ out.push({ product: t.product, seededTo: null });
1485
+ }
1486
+ }
1487
+ return out;
1488
+ }
1489
+
1490
+ // src/errors.ts
1491
+ var AppError = class extends Error {
1492
+ code;
1493
+ hint;
1494
+ cause;
1495
+ retryable;
1496
+ constructor(opts) {
1497
+ super(opts.message);
1498
+ this.name = "AppError";
1499
+ this.code = opts.code;
1500
+ this.hint = opts.hint;
1501
+ this.cause = opts.cause;
1502
+ this.retryable = opts.retryable ?? false;
1503
+ }
1504
+ /** Return the structured JSON shape (useful for --json output and MCP results). */
1505
+ toJSON() {
1506
+ return {
1507
+ code: this.code,
1508
+ message: this.message,
1509
+ hint: this.hint,
1510
+ ...this.cause ? { cause: this.cause } : {},
1511
+ ...this.retryable ? { retryable: this.retryable } : {}
1512
+ };
1513
+ }
1514
+ };
1515
+ function formatError(err, verbose = false) {
1516
+ if (err instanceof AppError) {
1517
+ const lines = [`\u2717 [${err.code}] ${err.message}`];
1518
+ lines.push(` hint: ${err.hint}`);
1519
+ if (verbose && err.cause) {
1520
+ lines.push(` cause: ${err.cause}`);
1521
+ }
1522
+ return lines.join("\n");
1523
+ }
1524
+ if (err instanceof Error) {
1525
+ return `\u2717 ${err.message}`;
1526
+ }
1527
+ return `\u2717 ${String(err)}`;
1528
+ }
1529
+
1530
+ // src/cli.ts
1531
+ var LOG_LEVELS = { silent: 0, normal: 1, verbose: 2, debug: 3 };
1532
+ function resolveLogLevel() {
1533
+ const env = (process.env.HK_LOG_LEVEL ?? "").toLowerCase();
1534
+ if (env in LOG_LEVELS) return env;
1535
+ if (process.env.HK_DEBUG) return "debug";
1536
+ return "normal";
1537
+ }
1538
+ var logLevel = resolveLogLevel();
1539
+ function logDebug(msg) {
1540
+ if (LOG_LEVELS[logLevel] >= LOG_LEVELS.debug) process.stderr.write(`[debug] ${msg}
1541
+ `);
1542
+ }
1543
+ var activeDb = null;
1544
+ function shutdown(signal) {
1545
+ if (activeDb) {
1546
+ try {
1547
+ activeDb.close();
1548
+ } catch {
1549
+ }
1550
+ activeDb = null;
1551
+ }
1552
+ const code = signal === "SIGINT" ? 130 : 143;
1553
+ process.exit(code);
1554
+ }
1555
+ process.on("SIGINT", () => shutdown("SIGINT"));
1556
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
1557
+ var cwd = process.cwd();
1558
+ var DEFAULT_DB = join4(cwd, "data", "claude-synergy.db");
1559
+ var DEFAULT_PRODUCTS = join4(cwd, "products");
1560
+ var _require = createRequire(import.meta.url);
1561
+ var { version: PKG_VERSION } = _require("../package.json");
1562
+ function intOpt(name, raw, defaultValue, min = 1, max = 1e4) {
1563
+ if (raw === void 0) return defaultValue;
1564
+ const n = parseInt(raw, 10);
1565
+ if (!Number.isFinite(n) || n < min || n > max) {
1566
+ console.error(`error: --${name} must be an integer in [${min}, ${max}] (got: ${raw})`);
1567
+ process.exit(1);
1568
+ }
1569
+ return n;
1570
+ }
1571
+ function openTrackedDb(path) {
1572
+ const db = openDb(path);
1573
+ activeDb = db;
1574
+ return db;
1575
+ }
1576
+ var CONTEXT_PROVIDERS = ["none", "structured", "ollama", "claude-haiku"];
1577
+ var EMBED_PROVIDERS = ["ollama", "voyage"];
1578
+ var RERANK_PROVIDERS = ["none", "ollama-judge", "voyage", "cohere"];
1579
+ function isDbEmpty(db) {
1580
+ try {
1581
+ const row = db.prepare("SELECT COUNT(*) AS n FROM changes").get();
1582
+ return !row || row.n === 0;
1583
+ } catch {
1584
+ return true;
1585
+ }
1586
+ }
1587
+ function warnIfEmpty(db) {
1588
+ if (isDbEmpty(db)) {
1589
+ console.error("No data yet. Run `hk sync` to fetch changelogs, then `hk query <term>` to search.");
1590
+ return true;
1591
+ }
1592
+ return false;
1593
+ }
1594
+ function fmtNum(n) {
1595
+ return n.toLocaleString("en-US");
1596
+ }
1597
+ function makeFetchProgressWriter() {
1598
+ return (event) => {
1599
+ const idx = `${event.index + 1}/${event.total}`;
1600
+ switch (event.type) {
1601
+ case "start":
1602
+ process.stderr.write(`Fetching ${event.product} (${idx})...
1603
+ `);
1604
+ break;
1605
+ case "error":
1606
+ process.stderr.write(`\u2717 ${event.product}: ${event.error ?? "unknown error"}
1607
+ `);
1608
+ break;
1609
+ case "skip":
1610
+ process.stderr.write(`\u2298 ${event.product} (skipped \u2014 ${event.error ?? "unknown"})
1611
+ `);
1612
+ break;
1613
+ }
1614
+ };
1615
+ }
1616
+ function printFetchSummary(summary) {
1617
+ const parts = [
1618
+ `${fmtNum(summary.succeeded)} succeeded`,
1619
+ `${fmtNum(summary.failed)} failed`,
1620
+ `${fmtNum(summary.skipped)} skipped`,
1621
+ `${fmtNum(summary.newChanges)} new changes`
1622
+ ];
1623
+ process.stderr.write(`
1624
+ Sync complete: ${parts.join(", ")}
1625
+ `);
1626
+ if (summary.failed > 0 && summary.errors.length > 0) {
1627
+ const failedList = summary.errors.map((e) => `${e.product} (${e.error})`).join(", ");
1628
+ process.stderr.write(`Failed: ${failedList}
1629
+ `);
1630
+ }
1631
+ }
1632
+ var program = new Command();
1633
+ program.name("hk").description("Claude Synergy \u2014 local Anthropic changelog mirror + cross-product synergies").version(PKG_VERSION).enablePositionalOptions().option("--json", "Output results as JSON (for CI/scripting)").option("--verbose", "Show verbose output (also: HK_LOG_LEVEL=verbose)").option("--debug", "Show debug output including query parameters (also: HK_LOG_LEVEL=debug)").hook("preAction", () => {
1634
+ const opts = program.opts();
1635
+ if (opts.debug) logLevel = "debug";
1636
+ else if (opts.verbose) logLevel = "verbose";
1637
+ });
1638
+ program.command("init").description("Create the database file with the current schema").option("-d, --db <path>", "database path", DEFAULT_DB).action((opts) => {
1639
+ const db = openTrackedDb(opts.db);
1640
+ initSchema(db);
1641
+ console.log(`\u2713 initialized ${resolve2(opts.db)}`);
1642
+ db.close();
1643
+ });
1644
+ 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) => {
1645
+ if (!existsSync2(opts.products)) {
1646
+ console.error(`\u2717 products dir not found: ${opts.products}`);
1647
+ process.exit(1);
1648
+ }
1649
+ const db = openTrackedDb(opts.db);
1650
+ initSchema(db);
1651
+ const start = Date.now();
1652
+ const stats = ingestAll(db, opts.products);
1653
+ const ms = Date.now() - start;
1654
+ console.log(`\u2713 ingested ${stats.productsCount} products in ${ms}ms`);
1655
+ console.log(` releases: ${stats.releasesAdded}`);
1656
+ console.log(` changes: ${stats.changesAdded}`);
1657
+ console.log(` entities: ${stats.entitiesAdded}`);
1658
+ if (stats.errors.length > 0) {
1659
+ console.log(`
1660
+ \u2717 errors: ${stats.errors.length}`);
1661
+ stats.errors.slice(0, 5).forEach((e) => console.log(` ${e.file}: ${e.error}`));
1662
+ if (stats.errors.length > 5) {
1663
+ console.log(` ... and ${stats.errors.length - 5} more`);
1664
+ }
1665
+ }
1666
+ db.close();
1667
+ });
1668
+ program.command("query <text>").description('Full-text search across all change bullets (FTS5). Quote multi-word: hk query "managed agents"').option("-d, --db <path>", "database path", DEFAULT_DB).option("-p, --product <name>", "limit to one product").option("-s, --since <date>", "YYYY-MM-DD lower bound").option("-k, --kind <kind>", "added|fixed|breaking|deprecated|renamed|removed|improved|changed").option("-l, --limit <n>", "max results", "20").action((text, opts) => {
1669
+ logDebug(`text=${JSON.stringify(text)} opts=${JSON.stringify(opts)}`);
1670
+ const db = openTrackedDb(opts.db);
1671
+ if (warnIfEmpty(db)) {
1672
+ db.close();
1673
+ return;
1674
+ }
1675
+ const q = text;
1676
+ let results;
1677
+ try {
1678
+ results = searchChanges(db, q, {
1679
+ product: opts.product,
1680
+ since: opts.since,
1681
+ kind: opts.kind,
1682
+ limit: intOpt("limit", opts.limit, 20)
1683
+ });
1684
+ } catch (e) {
1685
+ const appErr = new AppError({
1686
+ code: "QUERY_FAILED",
1687
+ message: `query failed: ${e.message}`,
1688
+ hint: `FTS5 syntax \u2014 use double quotes for phrase: hk query '"AskUserQuestion"' or use plain words: hk query AskUserQuestion`,
1689
+ cause: e.message
1690
+ });
1691
+ if (program.opts().json) {
1692
+ console.log(JSON.stringify(appErr.toJSON()));
1693
+ } else {
1694
+ console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
1695
+ }
1696
+ db.close();
1697
+ process.exit(1);
1698
+ }
1699
+ if (program.opts().json) {
1700
+ console.log(JSON.stringify(results.map((r) => ({
1701
+ released_at: r.released_at,
1702
+ product: r.product,
1703
+ version: r.version,
1704
+ kind: r.kind,
1705
+ text: r.text,
1706
+ snippet: r.snippet
1707
+ }))));
1708
+ } else if (results.length === 0) {
1709
+ console.log("(no results)");
1710
+ } else {
1711
+ for (const r of results) {
1712
+ console.log(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]`);
1713
+ console.log(` ${r.snippet}`);
1714
+ }
1715
+ console.log(`
1716
+ ${results.length} result${results.length === 1 ? "" : "s"}`);
1717
+ }
1718
+ db.close();
1719
+ });
1720
+ program.command("env-var <name>").description("Find when an env var was introduced or last changed").option("-d, --db <path>", "database path", DEFAULT_DB).action((name, opts) => {
1721
+ const db = openTrackedDb(opts.db);
1722
+ if (warnIfEmpty(db)) {
1723
+ db.close();
1724
+ return;
1725
+ }
1726
+ const results = lookupEntity(db, "env_var", name);
1727
+ if (program.opts().json) {
1728
+ console.log(JSON.stringify(results.map((r) => ({
1729
+ released_at: r.released_at,
1730
+ product: r.product,
1731
+ version: r.version,
1732
+ kind: r.kind,
1733
+ text: r.text
1734
+ }))));
1735
+ } else {
1736
+ printEntityResults(name, "env var", results);
1737
+ }
1738
+ db.close();
1739
+ });
1740
+ program.command("command <slash>").description("Find a slash command's history").option("-d, --db <path>", "database path", DEFAULT_DB).action((slash, opts) => {
1741
+ const db = openTrackedDb(opts.db);
1742
+ if (warnIfEmpty(db)) {
1743
+ db.close();
1744
+ return;
1745
+ }
1746
+ const normalized = slash.startsWith("/") ? slash : "/" + slash;
1747
+ const results = lookupEntity(db, "slash_command", normalized);
1748
+ if (program.opts().json) {
1749
+ console.log(JSON.stringify(results.map((r) => ({
1750
+ released_at: r.released_at,
1751
+ product: r.product,
1752
+ version: r.version,
1753
+ kind: r.kind,
1754
+ text: r.text
1755
+ }))));
1756
+ } else {
1757
+ printEntityResults(normalized, "slash command", results);
1758
+ }
1759
+ db.close();
1760
+ });
1761
+ program.command("model <id>").description("Find a Claude model ID's history (deprecations, launches)").option("-d, --db <path>", "database path", DEFAULT_DB).action((id, opts) => {
1762
+ const db = openTrackedDb(opts.db);
1763
+ if (warnIfEmpty(db)) {
1764
+ db.close();
1765
+ return;
1766
+ }
1767
+ const results = lookupEntity(db, "model_id", id);
1768
+ if (program.opts().json) {
1769
+ console.log(JSON.stringify(results.map((r) => ({
1770
+ released_at: r.released_at,
1771
+ product: r.product,
1772
+ version: r.version,
1773
+ kind: r.kind,
1774
+ text: r.text
1775
+ }))));
1776
+ } else {
1777
+ printEntityResults(id, "model id", results);
1778
+ }
1779
+ db.close();
1780
+ });
1781
+ program.command("cve <id>").description("Find releases mentioning a specific CVE").option("-d, --db <path>", "database path", DEFAULT_DB).action((id, opts) => {
1782
+ const db = openTrackedDb(opts.db);
1783
+ if (warnIfEmpty(db)) {
1784
+ db.close();
1785
+ return;
1786
+ }
1787
+ const results = lookupEntity(db, "cve", id);
1788
+ if (program.opts().json) {
1789
+ console.log(JSON.stringify(results.map((r) => ({
1790
+ released_at: r.released_at,
1791
+ product: r.product,
1792
+ version: r.version,
1793
+ kind: r.kind,
1794
+ text: r.text
1795
+ }))));
1796
+ } else {
1797
+ printEntityResults(id, "CVE", results);
1798
+ }
1799
+ db.close();
1800
+ });
1801
+ program.command("seed-markers").description("Seed fetch markers from the current DB state (run once after study-swarm to enable incremental fetch)").option("-d, --db <path>", "database path", DEFAULT_DB).action((opts) => {
1802
+ const db = openTrackedDb(opts.db);
1803
+ initSchema(db);
1804
+ const results = seedMarkersFromDb(db);
1805
+ for (const r of results) {
1806
+ console.log(`${r.product.padEnd(35)} ${r.seededTo ? "\u2192 " + r.seededTo.split("T")[0] : "(no releases on disk)"}`);
1807
+ }
1808
+ console.log(`
1809
+ \u2713 seeded ${results.filter((r) => r.seededTo).length} marker${results.filter((r) => r.seededTo).length === 1 ? "" : "s"}`);
1810
+ db.close();
1811
+ });
1812
+ program.command("fetch").description("Pull new GitHub releases since last sync (Tier 1 sources only \u2014 10 SDK/CLI products)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-r, --products-root <path>", "products dir root", DEFAULT_PRODUCTS).option("-p, --product <name>", "limit to one product").option("--since <iso>", "override the stored marker (YYYY-MM-DD or full ISO)").action(async (opts) => {
1813
+ const db = openTrackedDb(opts.db);
1814
+ initSchema(db);
1815
+ try {
1816
+ const stats = await fetchAll(db, opts.productsRoot, {
1817
+ product: opts.product,
1818
+ sinceOverride: opts.since,
1819
+ onProgress: makeFetchProgressWriter()
1820
+ });
1821
+ for (const s of stats) {
1822
+ const status = s.fetched > 0 ? `+${s.fetched} new` : `current (since ${s.newSince})`;
1823
+ console.log(`${s.product.padEnd(35)} ${status}${s.latest ? ` latest: ${s.latest.split("T")[0]}` : ""}`);
1824
+ }
1825
+ if (program.opts().json) {
1826
+ console.log(JSON.stringify({ stats, summary: stats.summary }));
1827
+ } else {
1828
+ printFetchSummary(stats.summary);
1829
+ }
1830
+ if (stats.summary.failed > 0 && stats.summary.succeeded > 0) {
1831
+ process.exitCode = 3;
1832
+ } else if (stats.summary.failed > 0 && stats.summary.succeeded === 0) {
1833
+ process.exitCode = 2;
1834
+ }
1835
+ } finally {
1836
+ db.close();
1837
+ }
1838
+ });
1839
+ program.command("sync").description("Run fetch \u2192 ingest \u2192 embed in sequence (for daily cron / GH Action)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-r, --products-root <path>", "products dir", DEFAULT_PRODUCTS).option("--skip-fetch", "skip the fetch step (just ingest+embed existing files)").option("--skip-embed", "skip the embed step (fetch+ingest only)").addOption(
1840
+ new Option("-c, --context <provider>", "context provider for embed").default("structured").choices([...CONTEXT_PROVIDERS])
1841
+ ).addOption(
1842
+ new Option("-e, --embed-provider <provider>", "embedding provider").default("ollama").choices([...EMBED_PROVIDERS])
1843
+ ).action(
1844
+ async (opts) => {
1845
+ const db = openTrackedDb(opts.db);
1846
+ initSchema(db);
1847
+ const t0 = Date.now();
1848
+ try {
1849
+ if (!opts.skipFetch) {
1850
+ process.stderr.write("=== fetch ===\n");
1851
+ const stats = await fetchAll(db, opts.productsRoot, {
1852
+ onProgress: makeFetchProgressWriter()
1853
+ });
1854
+ if (program.opts().json) {
1855
+ } else {
1856
+ printFetchSummary(stats.summary);
1857
+ }
1858
+ }
1859
+ process.stderr.write("\n=== ingest ===\n");
1860
+ const { ingestAll: ingestAll2 } = await import("./ingest-3LJNQWS7.js");
1861
+ const ingestStats = ingestAll2(db, opts.productsRoot);
1862
+ console.log(`ingested ${ingestStats.releasesAdded} releases, ${ingestStats.changesAdded} changes, ${ingestStats.entitiesAdded} entities`);
1863
+ if (!opts.skipEmbed) {
1864
+ process.stderr.write("\n=== embed ===\n");
1865
+ const embedStats = await embedAll(db, {
1866
+ contextProviderName: opts.context,
1867
+ embeddingProviderName: opts.embedProvider,
1868
+ onProgress: (p) => {
1869
+ const prov = p.provider ? ` [${p.provider}]` : "";
1870
+ const tokens = p.tokensUsed != null ? ` ${fmtNum(p.tokensUsed)} tokens` : "";
1871
+ const reqs = p.requestsMade != null ? ` ${p.requestsMade} reqs` : "";
1872
+ process.stderr.write(` Embedding batch ${p.batchesCompleted}/${p.batchesTotal} (${p.chunksCompleted}/${p.chunksTotal} chunks)${prov}${tokens}${reqs}
1873
+ `);
1874
+ }
1875
+ });
1876
+ console.log(`embedded ${embedStats.chunksCreated} new chunks via ${embedStats.contextProvider} + ${embedStats.embeddingProvider}`);
1877
+ }
1878
+ console.log(`
1879
+ \u2713 sync complete in ${Date.now() - t0}ms`);
1880
+ } catch (e) {
1881
+ const appErr = new AppError({
1882
+ code: "SYNC_FAILED",
1883
+ message: `sync failed: ${e.message}`,
1884
+ hint: "Check network connectivity and API credentials. Re-run with --verbose for details.",
1885
+ cause: e.message,
1886
+ retryable: true
1887
+ });
1888
+ if (program.opts().json) {
1889
+ console.log(JSON.stringify(appErr.toJSON()));
1890
+ } else {
1891
+ console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
1892
+ }
1893
+ process.exit(2);
1894
+ } finally {
1895
+ db.close();
1896
+ }
1897
+ }
1898
+ );
1899
+ program.command("embed").description("Generate contextual chunks + embeddings (Tier 2b \u2014 opt-in semantic layer)").option("-d, --db <path>", "database path", DEFAULT_DB).addOption(
1900
+ new Option("-c, --context <provider>", "context provider").default("structured").choices([...CONTEXT_PROVIDERS])
1901
+ ).addOption(
1902
+ new Option("-e, --embed <provider>", "embedding provider").default("ollama").choices([...EMBED_PROVIDERS])
1903
+ ).option("-p, --product <name>", "limit to one product").option("-l, --limit <n>", "embed at most N pending chunks (testing)").option("--batch-size <n>", "embedding batch size", "64").option("--force", "recompute even if chunk already exists").action(
1904
+ async (opts) => {
1905
+ const db = openTrackedDb(opts.db);
1906
+ try {
1907
+ const stats = await embedAll(db, {
1908
+ contextProviderName: opts.context,
1909
+ embeddingProviderName: opts.embed,
1910
+ product: opts.product,
1911
+ limit: opts.limit !== void 0 ? intOpt("limit", opts.limit, 1) : void 0,
1912
+ batchSize: intOpt("batch-size", opts.batchSize, 64),
1913
+ force: opts.force,
1914
+ onProgress: (p) => {
1915
+ const prov = p.provider ? ` [${p.provider}]` : "";
1916
+ const tokens = p.tokensUsed != null ? ` ${fmtNum(p.tokensUsed)} tokens` : "";
1917
+ const reqs = p.requestsMade != null ? ` ${p.requestsMade} reqs` : "";
1918
+ process.stderr.write(`Embedding batch ${p.batchesCompleted}/${p.batchesTotal} (${p.chunksCompleted}/${p.chunksTotal} chunks)${prov}${tokens}${reqs}
1919
+ `);
1920
+ }
1921
+ });
1922
+ console.log(`\u2713 embedded ${stats.chunksCreated} chunks in ${stats.totalMs}ms`);
1923
+ console.log(` context provider: ${stats.contextProvider} (${stats.contextMs}ms total)`);
1924
+ console.log(` embedding model: ${stats.embeddingProvider} (${stats.embedMs}ms total)`);
1925
+ if (stats.chunksCreated === 0) {
1926
+ console.log(` (nothing to do \u2014 all changes already embedded; use --force to re-embed)`);
1927
+ }
1928
+ } catch (e) {
1929
+ const hint = e.message.includes("Ollama") || e.message.includes("11434") ? "start Ollama with 'ollama serve' and pull the model: 'ollama pull nomic-embed-text'" : "Check provider connectivity. Re-run with --verbose for details.";
1930
+ const appErr = new AppError({
1931
+ code: "EMBED_FAILED",
1932
+ message: `embed failed: ${e.message}`,
1933
+ hint,
1934
+ cause: e.message,
1935
+ retryable: true
1936
+ });
1937
+ if (program.opts().json) {
1938
+ console.log(JSON.stringify(appErr.toJSON()));
1939
+ } else {
1940
+ console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
1941
+ }
1942
+ process.exit(2);
1943
+ } finally {
1944
+ db.close();
1945
+ }
1946
+ }
1947
+ );
1948
+ program.command("hybrid <text>").description("Hybrid FTS5 + sqlite-vec search via RRF, optional rerank (requires `hk embed` first)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-p, --product <name>", "limit to one product").option("-s, --since <date>", "YYYY-MM-DD lower bound").option("-k, --kind <kind>", "added|fixed|breaking|deprecated|renamed|removed|improved|changed").addOption(
1949
+ new Option("-e, --embed <provider>", "embedding provider for query").default("ollama").choices([...EMBED_PROVIDERS])
1950
+ ).addOption(
1951
+ new Option("-r, --rerank <provider>", "rerank provider").default("none").choices([...RERANK_PROVIDERS])
1952
+ ).option("-l, --limit <n>", "max results", "10").option("--top-k <n>", "per-channel pull before fusion", "60").option("--rerank-candidates <n>", "how many RRF candidates to rerank", "20").action(
1953
+ async (text, opts) => {
1954
+ const db = openTrackedDb(opts.db);
1955
+ try {
1956
+ const t0 = Date.now();
1957
+ const results = await hybridSearch(db, text, {
1958
+ product: opts.product,
1959
+ since: opts.since,
1960
+ kind: opts.kind,
1961
+ embedProviderName: opts.embed,
1962
+ rerankProviderName: opts.rerank,
1963
+ limit: intOpt("limit", opts.limit, 10),
1964
+ topK: intOpt("top-k", opts.topK, 60),
1965
+ rerankCandidates: intOpt("rerank-candidates", opts.rerankCandidates, 20)
1966
+ });
1967
+ const ms = Date.now() - t0;
1968
+ if (program.opts().json) {
1969
+ console.log(JSON.stringify(results.map((r) => ({
1970
+ released_at: r.released_at,
1971
+ product: r.product,
1972
+ version: r.version,
1973
+ kind: r.kind,
1974
+ text: r.text,
1975
+ rrf_score: r.rrf_score,
1976
+ rerank_score: r.rerank_score
1977
+ }))));
1978
+ } else if (results.length === 0) {
1979
+ console.log("(no results)");
1980
+ } else {
1981
+ for (const r of results) {
1982
+ const bm = r.bm25_rank ? `bm25#${r.bm25_rank}` : " ";
1983
+ const vec = r.vec_rank ? `vec#${r.vec_rank}` : " ";
1984
+ const rerank = r.rerank_score !== null ? ` rerank=${r.rerank_score.toFixed(2)}` : "";
1985
+ console.log(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}] ${bm} ${vec} rrf=${r.rrf_score.toFixed(4)}${rerank}`);
1986
+ console.log(` ${r.text.slice(0, 200)}${r.text.length > 200 ? "\u2026" : ""}`);
1987
+ }
1988
+ console.log(`
1989
+ ${results.length} result${results.length === 1 ? "" : "s"} in ${ms}ms (rerank: ${opts.rerank})`);
1990
+ }
1991
+ } catch (e) {
1992
+ const hint = e.message.includes("Ollama") || e.message.includes("11434") ? "start Ollama and pull the embedding model (and rerank model if using ollama-judge)" : "Check provider connectivity. Re-run with --verbose for details.";
1993
+ const appErr = new AppError({
1994
+ code: "HYBRID_QUERY_FAILED",
1995
+ message: `hybrid query failed: ${e.message}`,
1996
+ hint,
1997
+ cause: e.message,
1998
+ retryable: true
1999
+ });
2000
+ if (program.opts().json) {
2001
+ console.log(JSON.stringify(appErr.toJSON()));
2002
+ } else {
2003
+ console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
2004
+ }
2005
+ process.exit(2);
2006
+ } finally {
2007
+ db.close();
2008
+ }
2009
+ }
2010
+ );
2011
+ program.command("latest").description("Recent releases across all products (or one)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-p, --product <name>", "limit to one product").option("-l, --limit <n>", "max results", "20").action((opts) => {
2012
+ const db = openTrackedDb(opts.db);
2013
+ if (warnIfEmpty(db)) {
2014
+ db.close();
2015
+ return;
2016
+ }
2017
+ const releases = recentReleases(db, opts.product, intOpt("limit", opts.limit, 20));
2018
+ if (program.opts().json) {
2019
+ console.log(JSON.stringify(releases));
2020
+ } else if (releases.length === 0) {
2021
+ console.log("(no releases)");
2022
+ } else {
2023
+ for (const r of releases) {
2024
+ console.log(`${r.released_at} ${r.product}@${r.version} (${r.change_count} change${r.change_count === 1 ? "" : "s"})`);
2025
+ }
2026
+ }
2027
+ db.close();
2028
+ });
2029
+ program.command("products").description("List all products in the DB with release counts").option("-d, --db <path>", "database path", DEFAULT_DB).action((opts) => {
2030
+ const db = openTrackedDb(opts.db);
2031
+ if (warnIfEmpty(db)) {
2032
+ db.close();
2033
+ return;
2034
+ }
2035
+ const products = listProducts(db);
2036
+ if (program.opts().json) {
2037
+ console.log(JSON.stringify(products));
2038
+ } else if (products.length === 0) {
2039
+ console.log("(no products)");
2040
+ } else {
2041
+ console.log("Product Releases Latest");
2042
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2043
+ for (const p of products) {
2044
+ const latest = p.latest_version ? `${p.latest_version} (${p.latest_date})` : "\u2014";
2045
+ console.log(`${p.name.padEnd(36)} ${String(p.release_count).padStart(8)} ${latest}`);
2046
+ }
2047
+ }
2048
+ db.close();
2049
+ });
2050
+ program.command("top <entity-type>").description("Most-mentioned entities of a type (env_var, slash_command, cli_option, model_id, beta_header, cve, ghsa, hook_event, setting_key)").option("-d, --db <path>", "database path", DEFAULT_DB).option("-l, --limit <n>", "max results", "30").action((entityType, opts) => {
2051
+ const db = openTrackedDb(opts.db);
2052
+ if (warnIfEmpty(db)) {
2053
+ db.close();
2054
+ return;
2055
+ }
2056
+ const results = entityFrequency(db, entityType, intOpt("limit", opts.limit, 30));
2057
+ if (program.opts().json) {
2058
+ console.log(JSON.stringify(results));
2059
+ } else if (results.length === 0) {
2060
+ console.log(`(no entities of type "${entityType}")`);
2061
+ } else {
2062
+ for (const r of results) {
2063
+ console.log(`${String(r.count).padStart(4)} ${r.first_seen ?? "????-??-??"} ${r.value}`);
2064
+ }
2065
+ }
2066
+ db.close();
2067
+ });
2068
+ function printEntityResults(name, label, results) {
2069
+ if (results.length === 0) {
2070
+ console.log(`(${label} not found: ${name})`);
2071
+ return;
2072
+ }
2073
+ console.log(`${label} ${name} \u2014 ${results.length} mention${results.length === 1 ? "" : "s"}:
2074
+ `);
2075
+ for (const r of results) {
2076
+ console.log(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]`);
2077
+ console.log(` ${r.text}`);
2078
+ }
2079
+ }
2080
+ program.parseAsync(process.argv).catch((e) => {
2081
+ console.error(formatError(e, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
2082
+ if (activeDb) {
2083
+ try {
2084
+ activeDb.close();
2085
+ } catch {
2086
+ }
2087
+ activeDb = null;
2088
+ }
2089
+ process.exitCode = 2;
2090
+ });