@mcptoolshop/claude-synergy 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/CONTRIBUTING.md +5 -4
- package/README.es.md +78 -26
- package/README.fr.md +77 -25
- package/README.hi.md +78 -26
- package/README.it.md +75 -23
- package/README.ja.md +78 -26
- package/README.md +78 -27
- package/README.pt-BR.md +77 -25
- package/README.zh.md +77 -25
- package/dist/chunk-H3466JDH.js +1564 -0
- package/dist/{chunk-HCIZPSW4.js → chunk-HZEQG3WT.js} +281 -1
- package/dist/cli.js +279 -457
- package/dist/ingest-Z45YH7OX.js +8 -0
- package/dist/mcp-server.js +252 -17
- package/package.json +1 -1
- package/products.yaml +12 -6
- package/schema-vec.sql +9 -5
- package/dist/chunk-YFGUTT22.js +0 -754
- package/dist/ingest-3LJNQWS7.js +0 -6
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
AppError,
|
|
4
|
+
browseChanges,
|
|
5
|
+
embedAll,
|
|
5
6
|
entityFrequency,
|
|
7
|
+
formatError,
|
|
8
|
+
getChangesSince,
|
|
6
9
|
hybridSearch,
|
|
7
10
|
initSchema,
|
|
8
11
|
listProducts,
|
|
@@ -10,411 +13,22 @@ import {
|
|
|
10
13
|
openDb,
|
|
11
14
|
recentReleases,
|
|
12
15
|
searchChanges
|
|
13
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-H3466JDH.js";
|
|
14
17
|
import {
|
|
15
18
|
ingestAll,
|
|
16
19
|
loadProductsConfig
|
|
17
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-HZEQG3WT.js";
|
|
18
21
|
|
|
19
22
|
// src/cli.ts
|
|
20
23
|
import { Command, Option } from "commander";
|
|
21
|
-
import { join as
|
|
24
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
22
25
|
import { existsSync as existsSync2 } from "fs";
|
|
23
26
|
import { createRequire } from "module";
|
|
24
27
|
|
|
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
28
|
// src/fetch.ts
|
|
415
29
|
import { execFileSync } from "child_process";
|
|
416
30
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
417
|
-
import { join as
|
|
31
|
+
import { join as join2, resolve } from "path";
|
|
418
32
|
|
|
419
33
|
// src/fetch-rss.ts
|
|
420
34
|
import { XMLParser } from "fast-xml-parser";
|
|
@@ -660,6 +274,51 @@ function parseAiderHistory(md) {
|
|
|
660
274
|
flush();
|
|
661
275
|
return out.filter((i) => i.body.length > 0 || i.version !== "main");
|
|
662
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
|
+
}
|
|
663
322
|
function aiderReleaseDates() {
|
|
664
323
|
try {
|
|
665
324
|
const out = execSync(`gh api "repos/Aider-AI/aider/releases?per_page=100"`, {
|
|
@@ -902,7 +561,7 @@ async function fetchHtmlReleases(parser, sinceIso, signal) {
|
|
|
902
561
|
|
|
903
562
|
// src/fetch-mcp-registry.ts
|
|
904
563
|
import { writeFileSync, mkdirSync } from "fs";
|
|
905
|
-
import { join
|
|
564
|
+
import { join } from "path";
|
|
906
565
|
var UA2 = { "user-agent": "claude-synergy/0.1.0", accept: "application/json" };
|
|
907
566
|
async function fetchOfficialMcpRegistry(opts = {}) {
|
|
908
567
|
const maxPages = opts.maxPages ?? 50;
|
|
@@ -974,7 +633,7 @@ async function fetchSmitheryRegistry(opts = {}) {
|
|
|
974
633
|
}
|
|
975
634
|
function writeCatalog(productDir, productName, entries) {
|
|
976
635
|
mkdirSync(productDir, { recursive: true });
|
|
977
|
-
const catalogPath =
|
|
636
|
+
const catalogPath = join(productDir, "CATALOG.md");
|
|
978
637
|
const lines = [];
|
|
979
638
|
lines.push(`# ${productName} \u2014 MCP server catalog`);
|
|
980
639
|
lines.push("");
|
|
@@ -1044,6 +703,9 @@ function assertSafeFilename(name, context) {
|
|
|
1044
703
|
}
|
|
1045
704
|
var HARDCODED_FALLBACK_TARGETS = [
|
|
1046
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" },
|
|
1047
709
|
{ product: "claude-agent-sdk-python", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-python" },
|
|
1048
710
|
{ product: "claude-agent-sdk-typescript", strategy: "gh-releases", repo: "anthropics/claude-agent-sdk-typescript" },
|
|
1049
711
|
{ product: "anthropic-cli", strategy: "gh-releases", repo: "anthropics/anthropic-cli" },
|
|
@@ -1193,8 +855,8 @@ async function fetchGhReleases(db, outDir, target, since) {
|
|
|
1193
855
|
assertSafeFilename(filename, `gh-releases filename for tag ${JSON.stringify(r.tag_name)}`);
|
|
1194
856
|
const vName = sanitizeFilename(`v${baseName}`);
|
|
1195
857
|
assertSafeFilename(vName, `gh-releases vName for tag ${JSON.stringify(r.tag_name)}`);
|
|
1196
|
-
const path =
|
|
1197
|
-
const vPath =
|
|
858
|
+
const path = join2(outDir, `${filename}.md`);
|
|
859
|
+
const vPath = join2(outDir, `${vName}.md`);
|
|
1198
860
|
if (existsSync(path) || existsSync(vPath)) {
|
|
1199
861
|
if (!latest || r.published_at > latest) latest = r.published_at;
|
|
1200
862
|
continue;
|
|
@@ -1223,7 +885,7 @@ function ghReleases(repo, sinceIso) {
|
|
|
1223
885
|
);
|
|
1224
886
|
} catch (e) {
|
|
1225
887
|
const stderr = (e.stderr ?? "").toString();
|
|
1226
|
-
if (stderr.includes("404")) return page === 1 ? [] : all;
|
|
888
|
+
if (stderr.includes("404")) return page === 1 ? [] : filterAndShape(all, sinceIso);
|
|
1227
889
|
if (stderr.includes("403") || stderr.includes("429") || stderr.includes("rate limit")) {
|
|
1228
890
|
throw new Error(
|
|
1229
891
|
`GitHub API rate limit hit for ${repo}. Set GITHUB_TOKEN env var for 5000 req/hr (vs 60 unauthenticated). Original error: ${e.message}`
|
|
@@ -1235,12 +897,19 @@ function ghReleases(repo, sinceIso) {
|
|
|
1235
897
|
try {
|
|
1236
898
|
batch = JSON.parse(out);
|
|
1237
899
|
} catch {
|
|
1238
|
-
return page === 1 ? [] : all;
|
|
900
|
+
return page === 1 ? [] : filterAndShape(all, sinceIso);
|
|
1239
901
|
}
|
|
1240
902
|
if (!Array.isArray(batch) || batch.length === 0) break;
|
|
1241
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
|
+
}
|
|
1242
908
|
if (batch.length < 100) break;
|
|
1243
909
|
}
|
|
910
|
+
return filterAndShape(all, sinceIso);
|
|
911
|
+
}
|
|
912
|
+
function filterAndShape(all, sinceIso) {
|
|
1244
913
|
return all.filter((r) => r.published_at && r.published_at > sinceIso).map((r) => ({
|
|
1245
914
|
tag_name: r.tag_name,
|
|
1246
915
|
published_at: r.published_at,
|
|
@@ -1286,7 +955,7 @@ async function fetchRss(db, outDir, target, since, signal) {
|
|
|
1286
955
|
try {
|
|
1287
956
|
const safeSlug = sanitizeFilename(item.slug);
|
|
1288
957
|
assertSafeFilename(safeSlug, `rss slug for ${JSON.stringify(item.slug)}`);
|
|
1289
|
-
const path =
|
|
958
|
+
const path = join2(outDir, `${safeSlug}.md`);
|
|
1290
959
|
if (existsSync(path)) {
|
|
1291
960
|
if (!latest || item.pubDate > latest) latest = item.pubDate;
|
|
1292
961
|
continue;
|
|
@@ -1318,10 +987,17 @@ async function fetchRss(db, outDir, target, since, signal) {
|
|
|
1318
987
|
}
|
|
1319
988
|
async function fetchRawChangelog(db, outDir, target, since, signal) {
|
|
1320
989
|
if (!target.rawChangelogUrl) throw new Error(`${target.product}: raw-changelog requires url`);
|
|
1321
|
-
|
|
1322
|
-
|
|
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}`);
|
|
1323
1000
|
}
|
|
1324
|
-
const items = await fetchAiderHistory(target.rawChangelogUrl, since, signal);
|
|
1325
1001
|
let latest = null;
|
|
1326
1002
|
let fetched = 0;
|
|
1327
1003
|
const errors = [];
|
|
@@ -1329,7 +1005,7 @@ async function fetchRawChangelog(db, outDir, target, since, signal) {
|
|
|
1329
1005
|
try {
|
|
1330
1006
|
const safeVersion = sanitizeFilename(item.version);
|
|
1331
1007
|
assertSafeFilename(safeVersion, `raw-changelog version for ${JSON.stringify(item.version)}`);
|
|
1332
|
-
const path =
|
|
1008
|
+
const path = join2(outDir, `${safeVersion}.md`);
|
|
1333
1009
|
if (existsSync(path)) {
|
|
1334
1010
|
if (item.releasedAt && (!latest || item.releasedAt > latest)) latest = item.releasedAt;
|
|
1335
1011
|
continue;
|
|
@@ -1368,7 +1044,7 @@ async function fetchHtmlScrape(db, outDir, target, since, signal) {
|
|
|
1368
1044
|
try {
|
|
1369
1045
|
const safeSlug = sanitizeFilename(item.slug);
|
|
1370
1046
|
assertSafeFilename(safeSlug, `html-scrape slug for ${JSON.stringify(item.slug)}`);
|
|
1371
|
-
const path =
|
|
1047
|
+
const path = join2(outDir, `${safeSlug}.md`);
|
|
1372
1048
|
if (existsSync(path)) {
|
|
1373
1049
|
if (!latest || item.pubDate > latest) latest = item.pubDate;
|
|
1374
1050
|
continue;
|
|
@@ -1408,7 +1084,7 @@ async function fetchPlaywrightStrategy(db, outDir, target, since) {
|
|
|
1408
1084
|
try {
|
|
1409
1085
|
const safeSlug = sanitizeFilename(item.slug);
|
|
1410
1086
|
assertSafeFilename(safeSlug, `playwright slug for ${JSON.stringify(item.slug)}`);
|
|
1411
|
-
const path =
|
|
1087
|
+
const path = join2(outDir, `${safeSlug}.md`);
|
|
1412
1088
|
if (existsSync(path)) {
|
|
1413
1089
|
if (!latest || item.pubDate > latest) latest = item.pubDate;
|
|
1414
1090
|
continue;
|
|
@@ -1487,46 +1163,6 @@ function seedMarkersFromDb(db) {
|
|
|
1487
1163
|
return out;
|
|
1488
1164
|
}
|
|
1489
1165
|
|
|
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
1166
|
// src/cli.ts
|
|
1531
1167
|
var LOG_LEVELS = { silent: 0, normal: 1, verbose: 2, debug: 3 };
|
|
1532
1168
|
function resolveLogLevel() {
|
|
@@ -1555,8 +1191,8 @@ function shutdown(signal) {
|
|
|
1555
1191
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1556
1192
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1557
1193
|
var cwd = process.cwd();
|
|
1558
|
-
var DEFAULT_DB =
|
|
1559
|
-
var DEFAULT_PRODUCTS =
|
|
1194
|
+
var DEFAULT_DB = join3(cwd, "data", "claude-synergy.db");
|
|
1195
|
+
var DEFAULT_PRODUCTS = join3(cwd, "products");
|
|
1560
1196
|
var _require = createRequire(import.meta.url);
|
|
1561
1197
|
var { version: PKG_VERSION } = _require("../package.json");
|
|
1562
1198
|
function intOpt(name, raw, defaultValue, min = 1, max = 1e4) {
|
|
@@ -1573,6 +1209,71 @@ function openTrackedDb(path) {
|
|
|
1573
1209
|
activeDb = db;
|
|
1574
1210
|
return db;
|
|
1575
1211
|
}
|
|
1212
|
+
function parseRelativeDate(input, now = /* @__PURE__ */ new Date()) {
|
|
1213
|
+
const trimmed = input.trim();
|
|
1214
|
+
if (trimmed.length === 0) {
|
|
1215
|
+
throw new AppError({
|
|
1216
|
+
code: "INVALID_DATE",
|
|
1217
|
+
message: "date is empty",
|
|
1218
|
+
hint: "Pass a date like 7d, 2w, 1m, 1y, or YYYY-MM-DD"
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const relMatch = /^(\d+)([dwmy])$/i.exec(trimmed);
|
|
1222
|
+
if (relMatch) {
|
|
1223
|
+
const n = parseInt(relMatch[1], 10);
|
|
1224
|
+
const unit = relMatch[2].toLowerCase();
|
|
1225
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
1226
|
+
throw new AppError({
|
|
1227
|
+
code: "INVALID_DATE",
|
|
1228
|
+
message: `invalid relative date: ${input}`,
|
|
1229
|
+
hint: "Relative dates must be a non-negative integer (e.g. 7d, 2w, 1m, 1y)"
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const d = new Date(now);
|
|
1233
|
+
switch (unit) {
|
|
1234
|
+
case "d":
|
|
1235
|
+
d.setUTCDate(d.getUTCDate() - n);
|
|
1236
|
+
break;
|
|
1237
|
+
case "w":
|
|
1238
|
+
d.setUTCDate(d.getUTCDate() - n * 7);
|
|
1239
|
+
break;
|
|
1240
|
+
case "m":
|
|
1241
|
+
d.setUTCMonth(d.getUTCMonth() - n);
|
|
1242
|
+
break;
|
|
1243
|
+
case "y":
|
|
1244
|
+
d.setUTCFullYear(d.getUTCFullYear() - n);
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
return d.toISOString().slice(0, 10);
|
|
1248
|
+
}
|
|
1249
|
+
const parsed = new Date(trimmed);
|
|
1250
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1251
|
+
throw new AppError({
|
|
1252
|
+
code: "INVALID_DATE",
|
|
1253
|
+
message: `invalid date: ${input}`,
|
|
1254
|
+
hint: "Use YYYY-MM-DD, a full ISO timestamp, or a relative form like 7d/2w/1m/1y"
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed;
|
|
1258
|
+
return parsed.toISOString();
|
|
1259
|
+
}
|
|
1260
|
+
function resolveDateOrExit(name, raw) {
|
|
1261
|
+
if (raw === void 0) return void 0;
|
|
1262
|
+
try {
|
|
1263
|
+
return parseRelativeDate(raw);
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
if (e instanceof AppError) {
|
|
1266
|
+
if (program.opts().json) {
|
|
1267
|
+
console.log(JSON.stringify(e.toJSON()));
|
|
1268
|
+
} else {
|
|
1269
|
+
console.error(formatError(e, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
|
|
1270
|
+
}
|
|
1271
|
+
} else {
|
|
1272
|
+
console.error(`error: --${name} ${e.message}`);
|
|
1273
|
+
}
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1576
1277
|
var CONTEXT_PROVIDERS = ["none", "structured", "ollama", "claude-haiku"];
|
|
1577
1278
|
var EMBED_PROVIDERS = ["ollama", "voyage"];
|
|
1578
1279
|
var RERANK_PROVIDERS = ["none", "ollama-judge", "voyage", "cohere"];
|
|
@@ -1665,7 +1366,7 @@ program.command("ingest").description("Parse products/*/releases/*.md and load i
|
|
|
1665
1366
|
}
|
|
1666
1367
|
db.close();
|
|
1667
1368
|
});
|
|
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
|
|
1369
|
+
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>", "lower bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-u, --until <date>", "upper bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-k, --kind <kind>", "added|fixed|breaking|deprecated|renamed|removed|improved|changed").option("-l, --limit <n>", "max results", "20").action((text, opts) => {
|
|
1669
1370
|
logDebug(`text=${JSON.stringify(text)} opts=${JSON.stringify(opts)}`);
|
|
1670
1371
|
const db = openTrackedDb(opts.db);
|
|
1671
1372
|
if (warnIfEmpty(db)) {
|
|
@@ -1673,11 +1374,14 @@ program.command("query <text>").description('Full-text search across all change
|
|
|
1673
1374
|
return;
|
|
1674
1375
|
}
|
|
1675
1376
|
const q = text;
|
|
1377
|
+
const since = resolveDateOrExit("since", opts.since);
|
|
1378
|
+
const until = resolveDateOrExit("until", opts.until);
|
|
1676
1379
|
let results;
|
|
1677
1380
|
try {
|
|
1678
1381
|
results = searchChanges(db, q, {
|
|
1679
1382
|
product: opts.product,
|
|
1680
|
-
since
|
|
1383
|
+
since,
|
|
1384
|
+
until,
|
|
1681
1385
|
kind: opts.kind,
|
|
1682
1386
|
limit: intOpt("limit", opts.limit, 20)
|
|
1683
1387
|
});
|
|
@@ -1857,7 +1561,7 @@ program.command("sync").description("Run fetch \u2192 ingest \u2192 embed in seq
|
|
|
1857
1561
|
}
|
|
1858
1562
|
}
|
|
1859
1563
|
process.stderr.write("\n=== ingest ===\n");
|
|
1860
|
-
const { ingestAll: ingestAll2 } = await import("./ingest-
|
|
1564
|
+
const { ingestAll: ingestAll2 } = await import("./ingest-Z45YH7OX.js");
|
|
1861
1565
|
const ingestStats = ingestAll2(db, opts.productsRoot);
|
|
1862
1566
|
console.log(`ingested ${ingestStats.releasesAdded} releases, ${ingestStats.changesAdded} changes, ${ingestStats.entitiesAdded} entities`);
|
|
1863
1567
|
if (!opts.skipEmbed) {
|
|
@@ -1945,18 +1649,21 @@ program.command("embed").description("Generate contextual chunks + embeddings (T
|
|
|
1945
1649
|
}
|
|
1946
1650
|
}
|
|
1947
1651
|
);
|
|
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
|
|
1652
|
+
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>", "lower bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-u, --until <date>", "upper bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-k, --kind <kind>", "added|fixed|breaking|deprecated|renamed|removed|improved|changed").addOption(
|
|
1949
1653
|
new Option("-e, --embed <provider>", "embedding provider for query").default("ollama").choices([...EMBED_PROVIDERS])
|
|
1950
1654
|
).addOption(
|
|
1951
1655
|
new Option("-r, --rerank <provider>", "rerank provider").default("none").choices([...RERANK_PROVIDERS])
|
|
1952
1656
|
).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
1657
|
async (text, opts) => {
|
|
1954
1658
|
const db = openTrackedDb(opts.db);
|
|
1659
|
+
const since = resolveDateOrExit("since", opts.since);
|
|
1660
|
+
const until = resolveDateOrExit("until", opts.until);
|
|
1955
1661
|
try {
|
|
1956
1662
|
const t0 = Date.now();
|
|
1957
1663
|
const results = await hybridSearch(db, text, {
|
|
1958
1664
|
product: opts.product,
|
|
1959
|
-
since
|
|
1665
|
+
since,
|
|
1666
|
+
until,
|
|
1960
1667
|
kind: opts.kind,
|
|
1961
1668
|
embedProviderName: opts.embed,
|
|
1962
1669
|
rerankProviderName: opts.rerank,
|
|
@@ -2008,13 +1715,14 @@ ${results.length} result${results.length === 1 ? "" : "s"} in ${ms}ms (rerank: $
|
|
|
2008
1715
|
}
|
|
2009
1716
|
}
|
|
2010
1717
|
);
|
|
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) => {
|
|
1718
|
+
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("-s, --since <date>", "lower bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-l, --limit <n>", "max results", "20").action((opts) => {
|
|
2012
1719
|
const db = openTrackedDb(opts.db);
|
|
2013
1720
|
if (warnIfEmpty(db)) {
|
|
2014
1721
|
db.close();
|
|
2015
1722
|
return;
|
|
2016
1723
|
}
|
|
2017
|
-
const
|
|
1724
|
+
const since = resolveDateOrExit("since", opts.since);
|
|
1725
|
+
const releases = recentReleases(db, opts.product, intOpt("limit", opts.limit, 20), since);
|
|
2018
1726
|
if (program.opts().json) {
|
|
2019
1727
|
console.log(JSON.stringify(releases));
|
|
2020
1728
|
} else if (releases.length === 0) {
|
|
@@ -2047,6 +1755,117 @@ program.command("products").description("List all products in the DB with releas
|
|
|
2047
1755
|
}
|
|
2048
1756
|
db.close();
|
|
2049
1757
|
});
|
|
1758
|
+
program.command("diff [product]").description("Show changes in a date window, grouped by release. Default: last 7 days across all products.").option("-d, --db <path>", "database path", DEFAULT_DB).option("-s, --since <date>", "lower bound (YYYY-MM-DD or relative like 7d, 2w, 1m)", "7d").option("-u, --until <date>", "upper bound (YYYY-MM-DD or relative); default: now").option("-k, --kind <kind>", "added|fixed|breaking|deprecated|renamed|removed|improved|changed").option("-l, --limit <n>", "max change rows", "200").action((product, opts) => {
|
|
1759
|
+
const db = openTrackedDb(opts.db);
|
|
1760
|
+
if (warnIfEmpty(db)) {
|
|
1761
|
+
db.close();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const since = resolveDateOrExit("since", opts.since);
|
|
1765
|
+
const until = resolveDateOrExit("until", opts.until);
|
|
1766
|
+
if (!since) {
|
|
1767
|
+
console.error("error: --since is required");
|
|
1768
|
+
db.close();
|
|
1769
|
+
process.exit(1);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
let results;
|
|
1773
|
+
try {
|
|
1774
|
+
results = getChangesSince(db, {
|
|
1775
|
+
since,
|
|
1776
|
+
until,
|
|
1777
|
+
product,
|
|
1778
|
+
kind: opts.kind,
|
|
1779
|
+
limit: intOpt("limit", opts.limit, 200, 1, 1e4)
|
|
1780
|
+
});
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
const appErr = new AppError({
|
|
1783
|
+
code: "DIFF_FAILED",
|
|
1784
|
+
message: `diff failed: ${e.message}`,
|
|
1785
|
+
hint: "Verify --since / --until parse correctly and the DB is initialized.",
|
|
1786
|
+
cause: e.message
|
|
1787
|
+
});
|
|
1788
|
+
if (program.opts().json) console.log(JSON.stringify(appErr.toJSON()));
|
|
1789
|
+
else console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
|
|
1790
|
+
db.close();
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (program.opts().json) {
|
|
1795
|
+
console.log(JSON.stringify(results));
|
|
1796
|
+
} else if (results.length === 0) {
|
|
1797
|
+
const scope = product ? ` for product "${product}"` : "";
|
|
1798
|
+
console.log(`(no changes in window since ${since}${until ? ` until ${until}` : ""}${scope})`);
|
|
1799
|
+
console.log("Try widening the date window: hk diff --since 30d, or remove --kind / --product filters.");
|
|
1800
|
+
} else {
|
|
1801
|
+
let totalChanges = 0;
|
|
1802
|
+
for (const rel of results) {
|
|
1803
|
+
const date = rel.released_at ?? "????-??-??";
|
|
1804
|
+
console.log(`
|
|
1805
|
+
${date} ${rel.product}@${rel.version} (${rel.changes.length} change${rel.changes.length === 1 ? "" : "s"})`);
|
|
1806
|
+
for (const c of rel.changes) {
|
|
1807
|
+
console.log(` - [${c.kind}] ${c.text}`);
|
|
1808
|
+
}
|
|
1809
|
+
totalChanges += rel.changes.length;
|
|
1810
|
+
}
|
|
1811
|
+
const sinceLbl = opts.since;
|
|
1812
|
+
const untilLbl = opts.until ? ` until ${opts.until}` : "";
|
|
1813
|
+
console.log(`
|
|
1814
|
+
${totalChanges} change${totalChanges === 1 ? "" : "s"} across ${results.length} release${results.length === 1 ? "" : "s"} since ${sinceLbl}${untilLbl}`);
|
|
1815
|
+
}
|
|
1816
|
+
db.close();
|
|
1817
|
+
});
|
|
1818
|
+
program.command("breaking").description("List breaking changes, most recent first. Filter by product / date window.").option("-d, --db <path>", "database path", DEFAULT_DB).option("-p, --product <name>", "limit to one product").option("-s, --since <date>", "lower bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-u, --until <date>", "upper bound (YYYY-MM-DD or relative like 7d, 2w, 1m)").option("-l, --limit <n>", "max results", "50").action((opts) => {
|
|
1819
|
+
const db = openTrackedDb(opts.db);
|
|
1820
|
+
if (warnIfEmpty(db)) {
|
|
1821
|
+
db.close();
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const since = resolveDateOrExit("since", opts.since);
|
|
1825
|
+
const until = resolveDateOrExit("until", opts.until);
|
|
1826
|
+
let results;
|
|
1827
|
+
try {
|
|
1828
|
+
results = browseChanges(db, {
|
|
1829
|
+
product: opts.product,
|
|
1830
|
+
since,
|
|
1831
|
+
until,
|
|
1832
|
+
kind: "breaking",
|
|
1833
|
+
limit: intOpt("limit", opts.limit, 50, 1, 1e4)
|
|
1834
|
+
});
|
|
1835
|
+
} catch (e) {
|
|
1836
|
+
const appErr = new AppError({
|
|
1837
|
+
code: "BREAKING_FAILED",
|
|
1838
|
+
message: `breaking query failed: ${e.message}`,
|
|
1839
|
+
hint: "Verify --since / --until parse correctly and the DB is initialized.",
|
|
1840
|
+
cause: e.message
|
|
1841
|
+
});
|
|
1842
|
+
if (program.opts().json) console.log(JSON.stringify(appErr.toJSON()));
|
|
1843
|
+
else console.error(formatError(appErr, LOG_LEVELS[logLevel] >= LOG_LEVELS.verbose));
|
|
1844
|
+
db.close();
|
|
1845
|
+
process.exit(1);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (program.opts().json) {
|
|
1849
|
+
console.log(JSON.stringify(results.map((r) => ({
|
|
1850
|
+
released_at: r.released_at,
|
|
1851
|
+
product: r.product,
|
|
1852
|
+
version: r.version,
|
|
1853
|
+
kind: r.kind,
|
|
1854
|
+
text: r.text
|
|
1855
|
+
}))));
|
|
1856
|
+
} else if (results.length === 0) {
|
|
1857
|
+
console.log("(no breaking changes)");
|
|
1858
|
+
console.log('Note: ingest may not have classified any changes as breaking yet \u2014 try `hk query "" --kind breaking` patterns, or widen --since.');
|
|
1859
|
+
} else {
|
|
1860
|
+
for (const r of results) {
|
|
1861
|
+
console.log(`${r.released_at ?? "????-??-??"} ${r.product}@${r.version} [${r.kind}]`);
|
|
1862
|
+
console.log(` ${r.text}`);
|
|
1863
|
+
}
|
|
1864
|
+
console.log(`
|
|
1865
|
+
${results.length} breaking change${results.length === 1 ? "" : "s"}`);
|
|
1866
|
+
}
|
|
1867
|
+
db.close();
|
|
1868
|
+
});
|
|
2050
1869
|
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
1870
|
const db = openTrackedDb(opts.db);
|
|
2052
1871
|
if (warnIfEmpty(db)) {
|
|
@@ -2088,3 +1907,6 @@ program.parseAsync(process.argv).catch((e) => {
|
|
|
2088
1907
|
}
|
|
2089
1908
|
process.exitCode = 2;
|
|
2090
1909
|
});
|
|
1910
|
+
export {
|
|
1911
|
+
parseRelativeDate
|
|
1912
|
+
};
|