@mcptoolshop/claude-synergy 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,475 +1,3 @@
1
- // src/errors.ts
2
- var AppError = class extends Error {
3
- code;
4
- hint;
5
- cause;
6
- retryable;
7
- constructor(opts) {
8
- super(opts.message);
9
- this.name = "AppError";
10
- this.code = opts.code;
11
- this.hint = opts.hint;
12
- this.cause = opts.cause;
13
- this.retryable = opts.retryable ?? false;
14
- }
15
- /** Return the structured JSON shape (useful for --json output and MCP results). */
16
- toJSON() {
17
- return {
18
- code: this.code,
19
- message: this.message,
20
- hint: this.hint,
21
- ...this.cause ? { cause: this.cause } : {},
22
- ...this.retryable ? { retryable: this.retryable } : {}
23
- };
24
- }
25
- };
26
- function formatError(err, verbose = false) {
27
- if (err instanceof AppError) {
28
- const lines = [`\u2717 [${err.code}] ${err.message}`];
29
- lines.push(` hint: ${err.hint}`);
30
- if (verbose && err.cause) {
31
- lines.push(` cause: ${err.cause}`);
32
- }
33
- return lines.join("\n");
34
- }
35
- if (err instanceof Error) {
36
- return `\u2717 ${err.message}`;
37
- }
38
- return `\u2717 ${String(err)}`;
39
- }
40
-
41
- // src/db.ts
42
- import Database from "better-sqlite3";
43
- import { readFileSync, mkdirSync } from "fs";
44
- import { fileURLToPath } from "url";
45
- import { dirname, join, resolve } from "path";
46
- import * as sqliteVec from "sqlite-vec";
47
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
48
- var SCHEMA_VERSION = 3;
49
- var DEFAULT_EMBEDDING_DIM = 768;
50
- function openDb(path, opts = {}) {
51
- const abs = resolve(path);
52
- mkdirSync(dirname(abs), { recursive: true });
53
- const db = new Database(abs);
54
- db.pragma("journal_mode = WAL");
55
- db.pragma("foreign_keys = ON");
56
- if (opts.loadVec !== false) {
57
- try {
58
- sqliteVec.load(db);
59
- } catch (e) {
60
- const quiet = process.env.HK_DEBUG_VEC_LOAD_FAIL_SILENT || process.env.CLAUDE_SYNERGY_QUIET || process.env.MCP_QUIET;
61
- if (!quiet) {
62
- console.error(`[warn] sqlite-vec load failed: ${e.message}`);
63
- }
64
- }
65
- }
66
- return db;
67
- }
68
- function initSchema(db, schemaPath) {
69
- const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='products'`).get();
70
- if (existing) {
71
- ensureSchemaVersion(db);
72
- return;
73
- }
74
- const resolved = schemaPath ?? resolveSchemaPath();
75
- const sql = readFileSync(resolved, "utf-8");
76
- db.exec(sql);
77
- ensureSchemaVersion(db);
78
- }
79
- function ensureSchemaVersion(db) {
80
- db.exec(`
81
- CREATE TABLE IF NOT EXISTS schema_meta (
82
- key TEXT PRIMARY KEY,
83
- value TEXT NOT NULL
84
- )
85
- `);
86
- const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'schema_version'`).get();
87
- const currentVersion = row ? parseInt(row.value, 10) : 0;
88
- if (currentVersion > SCHEMA_VERSION) {
89
- throw new Error(
90
- `Database schema version ${currentVersion} is newer than this tool supports (version ${SCHEMA_VERSION}). Please upgrade claude-synergy: npm update -g @mcptoolshop/claude-synergy`
91
- );
92
- }
93
- if (currentVersion < SCHEMA_VERSION) {
94
- migrateSchema(db, currentVersion, SCHEMA_VERSION);
95
- db.prepare(`
96
- INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', @version)
97
- `).run({ version: String(SCHEMA_VERSION) });
98
- }
99
- }
100
- function migrateSchema(db, fromVersion, toVersion) {
101
- if (fromVersion < 3 && toVersion >= 3) {
102
- const hasDim = db.prepare(`SELECT value FROM schema_meta WHERE key = 'embedding_dim'`).get();
103
- if (!hasDim) {
104
- db.prepare(`
105
- INSERT INTO schema_meta (key, value) VALUES ('embedding_dim', @dim)
106
- `).run({ dim: String(DEFAULT_EMBEDDING_DIM) });
107
- }
108
- }
109
- void toVersion;
110
- }
111
- function getEmbeddingDim(db) {
112
- try {
113
- const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'embedding_dim'`).get();
114
- if (!row) return null;
115
- const n = parseInt(row.value, 10);
116
- return Number.isFinite(n) && n > 0 ? n : null;
117
- } catch {
118
- return null;
119
- }
120
- }
121
- function setEmbeddingDim(db, dim) {
122
- if (!Number.isFinite(dim) || dim <= 0 || (dim | 0) !== dim) {
123
- throw new AppError({
124
- code: "EMBEDDING_DIM_INVALID",
125
- message: `embedding dimension must be a positive integer (got ${String(dim)})`,
126
- hint: "pass a positive integer dim, e.g. 768 (nomic-embed-text), 1536 (text-embedding-3-small), or 3072 (text-embedding-3-large)"
127
- });
128
- }
129
- db.exec(`
130
- CREATE TABLE IF NOT EXISTS schema_meta (
131
- key TEXT PRIMARY KEY,
132
- value TEXT NOT NULL
133
- )
134
- `);
135
- const existing = getEmbeddingDim(db);
136
- if (existing !== null && existing !== dim) {
137
- const hasChunks = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get();
138
- let chunkCount = 0;
139
- if (hasChunks) {
140
- const row = db.prepare(`SELECT COUNT(*) AS n FROM chunks`).get();
141
- chunkCount = row.n;
142
- }
143
- if (chunkCount > 0) {
144
- throw new AppError({
145
- code: "EMBEDDING_DIM_MISMATCH",
146
- message: `database is configured for ${existing}-d embeddings but the requested provider produces ${dim}-d vectors (${chunkCount} existing chunks)`,
147
- hint: `re-init the DB to switch dimensions: rm <db-path>, then 'hk init' and 'hk embed' again. Existing chunks would be invalid against the new vector index.`
148
- });
149
- }
150
- }
151
- db.prepare(`
152
- INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('embedding_dim', @dim)
153
- `).run({ dim: String(dim) });
154
- }
155
- function resolveSchemaPath() {
156
- const candidates = [
157
- join(__dirname2, "..", "schema.sql"),
158
- join(process.cwd(), "schema.sql")
159
- ];
160
- for (const p of candidates) {
161
- try {
162
- readFileSync(p);
163
- return p;
164
- } catch {
165
- }
166
- }
167
- throw new Error(`schema.sql not found in: ${candidates.join(", ")}`);
168
- }
169
-
170
- // src/query.ts
171
- function searchChanges(db, query, opts = {}) {
172
- const limit = opts.limit ?? 20;
173
- const filters = [];
174
- const params = { query, limit };
175
- if (opts.product) {
176
- filters.push("c.product = @product");
177
- params.product = opts.product;
178
- }
179
- if (opts.since) {
180
- filters.push("r.released_at >= @since");
181
- params.since = opts.since;
182
- }
183
- if (opts.until) {
184
- filters.push("r.released_at <= @until");
185
- params.until = opts.until;
186
- }
187
- if (opts.kind) {
188
- filters.push("c.kind = @kind");
189
- params.kind = opts.kind;
190
- }
191
- const where = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
192
- const sql = `
193
- SELECT c.product, c.version, r.released_at,
194
- c.ordinal, c.kind, c.text AS body,
195
- snippet(changes_fts, 0, '[[', ']]', '\u2026', 16) AS snippet
196
- FROM changes_fts
197
- JOIN changes c ON changes_fts.rowid = c.id
198
- JOIN releases r ON c.product = r.product AND c.version = r.version
199
- WHERE changes_fts MATCH @query
200
- ${where}
201
- ORDER BY r.released_at DESC, c.ordinal ASC
202
- LIMIT @limit
203
- `;
204
- const rows = db.prepare(sql).all(params);
205
- return rows.map((r) => ({ ...r, text: r.body }));
206
- }
207
- function lookupEntity(db, type, value) {
208
- const sql = `
209
- SELECT c.product, c.version, r.released_at, c.ordinal, c.kind, c.text,
210
- '' AS snippet
211
- FROM entities e
212
- JOIN changes c ON e.change_id = c.id
213
- JOIN releases r ON c.product = r.product AND c.version = r.version
214
- WHERE e.entity_type = ? AND e.entity_value = ?
215
- ORDER BY r.released_at ASC, c.product
216
- `;
217
- return db.prepare(sql).all(type, value);
218
- }
219
- function recentReleases(db, product, limit = 20, since) {
220
- const filters = [];
221
- const params = { limit };
222
- if (product) {
223
- filters.push("r.product = @product");
224
- params.product = product;
225
- }
226
- if (since) {
227
- filters.push("r.released_at >= @since");
228
- params.since = since;
229
- }
230
- const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
231
- const sql = `
232
- SELECT r.product, r.version, r.released_at, COUNT(c.id) AS change_count
233
- FROM releases r
234
- LEFT JOIN changes c ON c.product = r.product AND c.version = r.version
235
- ${where}
236
- GROUP BY r.product, r.version
237
- ORDER BY r.released_at DESC
238
- LIMIT @limit
239
- `;
240
- return db.prepare(sql).all(params);
241
- }
242
- function listProducts(db) {
243
- const sql = `
244
- SELECT p.name, p.display_name,
245
- COUNT(DISTINCT r.version) AS release_count,
246
- (SELECT version FROM releases r2 WHERE r2.product = p.name ORDER BY r2.released_at DESC LIMIT 1) AS latest_version,
247
- (SELECT released_at FROM releases r2 WHERE r2.product = p.name ORDER BY r2.released_at DESC LIMIT 1) AS latest_date
248
- FROM products p
249
- LEFT JOIN releases r ON r.product = p.name
250
- GROUP BY p.name
251
- ORDER BY release_count DESC
252
- `;
253
- return db.prepare(sql).all();
254
- }
255
- function entityFrequency(db, type, limit = 30) {
256
- const sql = `
257
- SELECT e.entity_value AS value,
258
- COUNT(*) AS count,
259
- MIN(r.released_at) AS first_seen
260
- FROM entities e
261
- JOIN changes c ON e.change_id = c.id
262
- JOIN releases r ON c.product = r.product AND c.version = r.version
263
- WHERE e.entity_type = ?
264
- GROUP BY e.entity_value
265
- ORDER BY count DESC, first_seen ASC
266
- LIMIT ?
267
- `;
268
- return db.prepare(sql).all(type, limit);
269
- }
270
- function browseChanges(db, opts = {}) {
271
- const limit = opts.limit ?? 20;
272
- const filters = [];
273
- const params = { limit };
274
- if (opts.product) {
275
- filters.push("c.product = @product");
276
- params.product = opts.product;
277
- }
278
- if (opts.since) {
279
- filters.push("r.released_at >= @since");
280
- params.since = opts.since;
281
- }
282
- if (opts.until) {
283
- filters.push("r.released_at <= @until");
284
- params.until = opts.until;
285
- }
286
- if (opts.kind) {
287
- filters.push("c.kind = @kind");
288
- params.kind = opts.kind;
289
- }
290
- const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
291
- const sql = `
292
- SELECT c.product, c.version, r.released_at,
293
- c.ordinal, c.kind, c.text AS body,
294
- '' AS snippet
295
- FROM changes c
296
- JOIN releases r ON c.product = r.product AND c.version = r.version
297
- ${where}
298
- ORDER BY r.released_at DESC, c.product, c.ordinal ASC
299
- LIMIT @limit
300
- `;
301
- const rows = db.prepare(sql).all(params);
302
- return rows.map((r) => ({ ...r, text: r.body }));
303
- }
304
- function getChangesSince(db, opts) {
305
- if (!opts.since) {
306
- throw new AppError({
307
- code: "QUERY_SINCE_REQUIRED",
308
- message: "getChangesSince requires a `since` date",
309
- hint: 'pass since as YYYY-MM-DD (e.g. "2026-01-01")'
310
- });
311
- }
312
- const limit = opts.limit ?? 200;
313
- const filters = ["r.released_at >= @since"];
314
- const params = { since: opts.since };
315
- if (opts.until) {
316
- filters.push("r.released_at <= @until");
317
- params.until = opts.until;
318
- }
319
- if (opts.product) {
320
- filters.push("c.product = @product");
321
- params.product = opts.product;
322
- }
323
- if (opts.kind) {
324
- filters.push("c.kind = @kind");
325
- params.kind = opts.kind;
326
- }
327
- const where = `WHERE ${filters.join(" AND ")}`;
328
- const sql = `
329
- SELECT c.product, c.version, r.released_at, c.ordinal, c.kind, c.text
330
- FROM changes c
331
- JOIN releases r ON c.product = r.product AND c.version = r.version
332
- ${where}
333
- ORDER BY r.released_at DESC, c.product, c.version, c.ordinal ASC
334
- `;
335
- const rows = db.prepare(sql).all(params);
336
- const grouped = /* @__PURE__ */ new Map();
337
- let total = 0;
338
- for (const r of rows) {
339
- if (total >= limit) break;
340
- const key = `${r.product}@${r.version}`;
341
- let bucket = grouped.get(key);
342
- if (!bucket) {
343
- bucket = {
344
- product: r.product,
345
- version: r.version,
346
- released_at: r.released_at,
347
- changes: []
348
- };
349
- grouped.set(key, bucket);
350
- }
351
- bucket.changes.push({ ordinal: r.ordinal, kind: r.kind, text: r.text });
352
- total++;
353
- }
354
- return Array.from(grouped.values());
355
- }
356
- function compareVersions(db, opts) {
357
- if (!opts.product || !opts.fromVersion || !opts.toVersion) {
358
- throw new AppError({
359
- code: "QUERY_COMPARE_ARGS",
360
- message: "compareVersions requires product, fromVersion, and toVersion",
361
- hint: 'pass all three as strings, e.g. { product: "claude-code", fromVersion: "2.1.100", toVersion: "2.1.147" }'
362
- });
363
- }
364
- const endpoint = db.prepare(`
365
- SELECT version, released_at FROM releases
366
- WHERE product = ? AND version = ?
367
- `);
368
- const fromRow = endpoint.get(opts.product, opts.fromVersion);
369
- const toRow = endpoint.get(opts.product, opts.toVersion);
370
- if (!fromRow || !toRow) {
371
- return [];
372
- }
373
- if (!fromRow.released_at || !toRow.released_at) {
374
- throw new AppError({
375
- code: "QUERY_VERSION_NO_DATE",
376
- message: `cannot compare versions without released_at on both endpoints (${opts.product}@${opts.fromVersion} or ${opts.toVersion} is missing a date)`,
377
- hint: "re-ingest the affected product so released_at is populated"
378
- });
379
- }
380
- const sql = `
381
- SELECT c.product, c.version, r.released_at, c.ordinal, c.kind, c.text
382
- FROM changes c
383
- JOIN releases r ON c.product = r.product AND c.version = r.version
384
- WHERE c.product = @product
385
- AND r.released_at > @fromDate
386
- AND r.released_at <= @toDate
387
- ORDER BY r.released_at DESC, c.version, c.ordinal ASC
388
- `;
389
- const rows = db.prepare(sql).all({
390
- product: opts.product,
391
- fromDate: fromRow.released_at,
392
- toDate: toRow.released_at
393
- });
394
- const grouped = /* @__PURE__ */ new Map();
395
- for (const r of rows) {
396
- const key = `${r.product}@${r.version}`;
397
- let bucket = grouped.get(key);
398
- if (!bucket) {
399
- bucket = {
400
- product: r.product,
401
- version: r.version,
402
- released_at: r.released_at,
403
- changes: []
404
- };
405
- grouped.set(key, bucket);
406
- }
407
- bucket.changes.push({ ordinal: r.ordinal, kind: r.kind, text: r.text });
408
- }
409
- return Array.from(grouped.values());
410
- }
411
- function listSynergies(db, opts = {}) {
412
- const filters = [];
413
- const params = {};
414
- if (opts.product) {
415
- filters.push("s.id IN (SELECT synergy_id FROM synergy_products WHERE product = @product)");
416
- params.product = opts.product;
417
- }
418
- if (opts.status) {
419
- filters.push("s.status = @status");
420
- params.status = opts.status;
421
- }
422
- const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
423
- const limitClause = opts.limit ? "LIMIT @limit" : "";
424
- if (opts.limit) params.limit = opts.limit;
425
- const sql = `
426
- SELECT s.id, s.name, s.title, s.trigger, s.status, s.last_validated, s.notes_path
427
- FROM synergies s
428
- ${where}
429
- ORDER BY s.last_validated DESC, s.name
430
- ${limitClause}
431
- `;
432
- const rows = db.prepare(sql).all(params);
433
- if (rows.length === 0) return [];
434
- const ids = rows.map((r) => r.id);
435
- const productsByIdSql = `
436
- SELECT synergy_id, product FROM synergy_products
437
- WHERE synergy_id IN (${ids.map(() => "?").join(",")})
438
- ORDER BY synergy_id, product
439
- `;
440
- const productRows = db.prepare(productsByIdSql).all(...ids);
441
- const productsById = /* @__PURE__ */ new Map();
442
- for (const pr of productRows) {
443
- if (!productsById.has(pr.synergy_id)) productsById.set(pr.synergy_id, []);
444
- productsById.get(pr.synergy_id).push(pr.product);
445
- }
446
- return rows.map((r) => ({ ...r, products: productsById.get(r.id) ?? [] }));
447
- }
448
- function getSynergy(db, name) {
449
- const synergy = db.prepare(`
450
- SELECT id, name, title, trigger, status, last_validated, notes_path
451
- FROM synergies WHERE name = ?
452
- `).get(name);
453
- if (!synergy) return null;
454
- const products = db.prepare(`SELECT product FROM synergy_products WHERE synergy_id = ? ORDER BY product`).all(synergy.id).map((r) => r.product);
455
- const steps = db.prepare(`SELECT ordinal, text FROM synergy_steps WHERE synergy_id = ? ORDER BY ordinal`).all(synergy.id);
456
- const evidence = db.prepare(`SELECT source_url, quote, source_kind FROM synergy_evidence WHERE synergy_id = ?`).all(synergy.id);
457
- const change_refs = db.prepare(`
458
- SELECT scr.change_id, c.product, c.version, c.text
459
- FROM synergy_change_refs scr
460
- JOIN changes c ON c.id = scr.change_id
461
- WHERE scr.synergy_id = ?
462
- ORDER BY c.product, c.version
463
- `).all(synergy.id);
464
- return {
465
- ...synergy,
466
- products,
467
- steps,
468
- evidence,
469
- change_refs
470
- };
471
- }
472
-
473
1
  // src/embed.ts
474
2
  import { readFileSync as readFileSync2 } from "fs";
475
3
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -916,26 +444,197 @@ var OpenAIEmbeddingProvider = class {
916
444
  }
917
445
  };
918
446
 
919
- // src/embed.ts
920
- var __dirname3 = dirname2(fileURLToPath2(import.meta.url));
921
- function initVecSchema(db, opts = {}) {
922
- if (opts.dim !== void 0) {
923
- setEmbeddingDim(db, opts.dim);
924
- }
925
- const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get();
926
- if (!existing) {
927
- const schemaPath = resolveSchemaVecPath();
928
- const sql = readFileSync2(schemaPath, "utf-8");
929
- db.exec(sql);
930
- }
931
- const hasVec = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks_vec'`).get();
932
- if (!hasVec) {
933
- const dim = getEmbeddingDim(db) ?? DEFAULT_EMBEDDING_DIM;
934
- db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(embedding FLOAT[${dim}])`);
935
- }
936
- }
937
- function resolveSchemaVecPath() {
938
- const candidates = [
447
+ // src/db.ts
448
+ import Database from "better-sqlite3";
449
+ import { readFileSync, mkdirSync } from "fs";
450
+ import { fileURLToPath } from "url";
451
+ import { dirname, join, resolve } from "path";
452
+ import * as sqliteVec from "sqlite-vec";
453
+
454
+ // src/errors.ts
455
+ var AppError = class extends Error {
456
+ code;
457
+ hint;
458
+ cause;
459
+ retryable;
460
+ constructor(opts) {
461
+ super(opts.message);
462
+ this.name = "AppError";
463
+ this.code = opts.code;
464
+ this.hint = opts.hint;
465
+ this.cause = opts.cause;
466
+ this.retryable = opts.retryable ?? false;
467
+ }
468
+ /** Return the structured JSON shape (useful for --json output and MCP results). */
469
+ toJSON() {
470
+ return {
471
+ code: this.code,
472
+ message: this.message,
473
+ hint: this.hint,
474
+ ...this.cause ? { cause: this.cause } : {},
475
+ ...this.retryable ? { retryable: this.retryable } : {}
476
+ };
477
+ }
478
+ };
479
+ function formatError(err, verbose = false) {
480
+ if (err instanceof AppError) {
481
+ const lines = [`\u2717 [${err.code}] ${err.message}`];
482
+ lines.push(` hint: ${err.hint}`);
483
+ if (verbose && err.cause) {
484
+ lines.push(` cause: ${err.cause}`);
485
+ }
486
+ return lines.join("\n");
487
+ }
488
+ if (err instanceof Error) {
489
+ return `\u2717 ${err.message}`;
490
+ }
491
+ return `\u2717 ${String(err)}`;
492
+ }
493
+
494
+ // src/db.ts
495
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
496
+ var SCHEMA_VERSION = 3;
497
+ var DEFAULT_EMBEDDING_DIM = 768;
498
+ function openDb(path, opts = {}) {
499
+ const abs = resolve(path);
500
+ mkdirSync(dirname(abs), { recursive: true });
501
+ const db = new Database(abs);
502
+ db.pragma("journal_mode = WAL");
503
+ db.pragma("foreign_keys = ON");
504
+ if (opts.loadVec !== false) {
505
+ try {
506
+ sqliteVec.load(db);
507
+ } catch (e) {
508
+ const quiet = process.env.HK_DEBUG_VEC_LOAD_FAIL_SILENT || process.env.CLAUDE_SYNERGY_QUIET || process.env.MCP_QUIET;
509
+ if (!quiet) {
510
+ console.error(`[warn] sqlite-vec load failed: ${e.message}`);
511
+ }
512
+ }
513
+ }
514
+ return db;
515
+ }
516
+ function initSchema(db, schemaPath) {
517
+ const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='products'`).get();
518
+ if (existing) {
519
+ ensureSchemaVersion(db);
520
+ return;
521
+ }
522
+ const resolved = schemaPath ?? resolveSchemaPath();
523
+ const sql = readFileSync(resolved, "utf-8");
524
+ db.exec(sql);
525
+ ensureSchemaVersion(db);
526
+ }
527
+ function ensureSchemaVersion(db) {
528
+ db.exec(`
529
+ CREATE TABLE IF NOT EXISTS schema_meta (
530
+ key TEXT PRIMARY KEY,
531
+ value TEXT NOT NULL
532
+ )
533
+ `);
534
+ const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'schema_version'`).get();
535
+ const currentVersion = row ? parseInt(row.value, 10) : 0;
536
+ if (currentVersion > SCHEMA_VERSION) {
537
+ throw new Error(
538
+ `Database schema version ${currentVersion} is newer than this tool supports (version ${SCHEMA_VERSION}). Please upgrade claude-synergy: npm update -g @mcptoolshop/claude-synergy`
539
+ );
540
+ }
541
+ if (currentVersion < SCHEMA_VERSION) {
542
+ migrateSchema(db, currentVersion, SCHEMA_VERSION);
543
+ db.prepare(`
544
+ INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', @version)
545
+ `).run({ version: String(SCHEMA_VERSION) });
546
+ }
547
+ }
548
+ function migrateSchema(db, fromVersion, toVersion) {
549
+ if (fromVersion < 3 && toVersion >= 3) {
550
+ const hasDim = db.prepare(`SELECT value FROM schema_meta WHERE key = 'embedding_dim'`).get();
551
+ if (!hasDim) {
552
+ db.prepare(`
553
+ INSERT INTO schema_meta (key, value) VALUES ('embedding_dim', @dim)
554
+ `).run({ dim: String(DEFAULT_EMBEDDING_DIM) });
555
+ }
556
+ }
557
+ void toVersion;
558
+ }
559
+ function getEmbeddingDim(db) {
560
+ try {
561
+ const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'embedding_dim'`).get();
562
+ if (!row) return null;
563
+ const n = parseInt(row.value, 10);
564
+ return Number.isFinite(n) && n > 0 ? n : null;
565
+ } catch {
566
+ return null;
567
+ }
568
+ }
569
+ function setEmbeddingDim(db, dim) {
570
+ if (!Number.isFinite(dim) || dim <= 0 || (dim | 0) !== dim) {
571
+ throw new AppError({
572
+ code: "EMBEDDING_DIM_INVALID",
573
+ message: `embedding dimension must be a positive integer (got ${String(dim)})`,
574
+ hint: "pass a positive integer dim, e.g. 768 (nomic-embed-text), 1536 (text-embedding-3-small), or 3072 (text-embedding-3-large)"
575
+ });
576
+ }
577
+ db.exec(`
578
+ CREATE TABLE IF NOT EXISTS schema_meta (
579
+ key TEXT PRIMARY KEY,
580
+ value TEXT NOT NULL
581
+ )
582
+ `);
583
+ const existing = getEmbeddingDim(db);
584
+ if (existing !== null && existing !== dim) {
585
+ const hasChunks = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get();
586
+ let chunkCount = 0;
587
+ if (hasChunks) {
588
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM chunks`).get();
589
+ chunkCount = row.n;
590
+ }
591
+ if (chunkCount > 0) {
592
+ throw new AppError({
593
+ code: "EMBEDDING_DIM_MISMATCH",
594
+ message: `database is configured for ${existing}-d embeddings but the requested provider produces ${dim}-d vectors (${chunkCount} existing chunks)`,
595
+ hint: `re-init the DB to switch dimensions: rm <db-path>, then 'hk init' and 'hk embed' again. Existing chunks would be invalid against the new vector index.`
596
+ });
597
+ }
598
+ }
599
+ db.prepare(`
600
+ INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('embedding_dim', @dim)
601
+ `).run({ dim: String(dim) });
602
+ }
603
+ function resolveSchemaPath() {
604
+ const candidates = [
605
+ join(__dirname2, "..", "schema.sql"),
606
+ join(process.cwd(), "schema.sql")
607
+ ];
608
+ for (const p of candidates) {
609
+ try {
610
+ readFileSync(p);
611
+ return p;
612
+ } catch {
613
+ }
614
+ }
615
+ throw new Error(`schema.sql not found in: ${candidates.join(", ")}`);
616
+ }
617
+
618
+ // src/embed.ts
619
+ var __dirname3 = dirname2(fileURLToPath2(import.meta.url));
620
+ function initVecSchema(db, opts = {}) {
621
+ if (opts.dim !== void 0) {
622
+ setEmbeddingDim(db, opts.dim);
623
+ }
624
+ const existing = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'`).get();
625
+ if (!existing) {
626
+ const schemaPath = resolveSchemaVecPath();
627
+ const sql = readFileSync2(schemaPath, "utf-8");
628
+ db.exec(sql);
629
+ }
630
+ const hasVec = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='chunks_vec'`).get();
631
+ if (!hasVec) {
632
+ const dim = getEmbeddingDim(db) ?? DEFAULT_EMBEDDING_DIM;
633
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(embedding FLOAT[${dim}])`);
634
+ }
635
+ }
636
+ function resolveSchemaVecPath() {
637
+ const candidates = [
939
638
  join2(__dirname3, "..", "schema-vec.sql"),
940
639
  join2(process.cwd(), "schema-vec.sql")
941
640
  ];
@@ -1162,403 +861,16 @@ function makeEmbeddingProvider(name, opts = {}) {
1162
861
  }
1163
862
  }
1164
863
 
1165
- // src/providers/rerank/ollama-judge.ts
1166
- function providerTimeoutMs5() {
1167
- const raw = process.env.CLAUDE_SYNERGY_PROVIDER_TIMEOUT_MS;
1168
- const n = raw === void 0 ? NaN : parseInt(raw, 10);
1169
- return Number.isFinite(n) && n > 0 ? n : 6e4;
1170
- }
1171
- async function safeErrorBody5(res, max = 200) {
1172
- try {
1173
- const body = await res.text();
1174
- const safe = body.split("\n").filter((l) => !/x-api-key|authorization|bearer|api[-_]?key/i.test(l)).join("\n");
1175
- return safe.slice(0, max);
1176
- } catch {
1177
- return "<unreadable>";
1178
- }
1179
- }
1180
- var OllamaJudgeRerankProvider = class {
1181
- name = "ollama-judge";
1182
- host;
1183
- model;
1184
- constructor(opts = {}) {
1185
- const rawHost = opts.host ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
1186
- this.host = /^https?:\/\//.test(rawHost) ? rawHost : `http://${rawHost}`;
1187
- this.model = opts.model ?? process.env.OLLAMA_RERANK_MODEL ?? "qwen3:8b";
1188
- }
1189
- async rerank(query, candidates) {
1190
- if (candidates.length === 0) return [];
1191
- const docs = candidates.map((c, i) => `Doc ${i + 1}: ${c.text.slice(0, 400)}`).join("\n\n");
1192
- const prompt = [
1193
- `You are a relevance judge for a Claude changelog search.`,
1194
- `Rate each document 0-10 for how well it answers the user's query.`,
1195
- `Respond with EXACTLY ${candidates.length} integers, one per line, in order. No prose, no explanations, no doc numbers, just one integer per line.`,
1196
- ``,
1197
- `Query: ${query}`,
1198
- ``,
1199
- docs,
1200
- ``,
1201
- `Scores (one integer per line, ${candidates.length} lines total):`
1202
- ].join("\n");
1203
- const timeoutMs = providerTimeoutMs5();
1204
- let res;
1205
- try {
1206
- res = await fetch(`${this.host}/api/generate`, {
1207
- method: "POST",
1208
- headers: { "content-type": "application/json" },
1209
- body: JSON.stringify({
1210
- model: this.model,
1211
- prompt,
1212
- stream: false,
1213
- think: false,
1214
- // for qwen3/o1-style models; harmless on others
1215
- options: { temperature: 0, num_predict: Math.max(120, candidates.length * 4) }
1216
- }),
1217
- signal: AbortSignal.timeout(timeoutMs)
1218
- });
1219
- } catch (e) {
1220
- if (e?.name === "TimeoutError" || e?.name === "AbortError") {
1221
- throw new Error(`Ollama judge rerank timed out after ${timeoutMs}ms \u2014 is the Ollama server responsive?`);
1222
- }
1223
- throw e;
1224
- }
1225
- if (!res.ok) throw new Error(`Ollama judge ${res.status}: ${await safeErrorBody5(res)}`);
1226
- const json = await res.json();
1227
- const scores = parseScores(json.response, candidates.length);
1228
- return candidates.map((c, i) => ({ id: c.id, score: scores[i] ?? 0 }));
1229
- }
1230
- };
1231
- function parseScores(response, n) {
1232
- const lines = response.split(/\r?\n/);
1233
- const out = [];
1234
- for (const line of lines) {
1235
- if (out.length >= n) break;
1236
- const m = line.match(/-?\d+(?:\.\d+)?/);
1237
- if (!m) continue;
1238
- const v = parseFloat(m[0]);
1239
- if (Number.isFinite(v)) out.push(Math.max(0, Math.min(10, v)));
1240
- }
1241
- while (out.length < n) out.push(0);
1242
- return out;
1243
- }
1244
-
1245
- // src/providers/rerank/voyage.ts
1246
- var VoyageRerankProvider = class {
1247
- name = "voyage";
1248
- apiKey;
1249
- model;
1250
- /** Accumulated usage across all rerank() calls on this instance. */
1251
- _usage = { tokens: 0, requests: 0 };
1252
- constructor(opts = {}) {
1253
- this.apiKey = opts.apiKey ?? process.env.VOYAGE_API_KEY ?? "";
1254
- this.model = opts.model ?? "rerank-2";
1255
- if (!this.apiKey) {
1256
- throw new Error("voyage rerank provider requires VOYAGE_API_KEY");
1257
- }
1258
- }
1259
- /** Get accumulated usage stats. */
1260
- get usage() {
1261
- return { ...this._usage };
1262
- }
1263
- /** Reset accumulated usage counters. */
1264
- resetUsage() {
1265
- this._usage = { tokens: 0, requests: 0 };
1266
- }
1267
- async rerank(query, candidates, signal) {
1268
- if (candidates.length === 0) return [];
1269
- const timeoutMs = providerTimeoutMs4();
1270
- const { data: result } = await withRetry(
1271
- async () => {
1272
- let res;
1273
- try {
1274
- res = await fetch("https://api.voyageai.com/v1/rerank", {
1275
- method: "POST",
1276
- headers: {
1277
- "authorization": `Bearer ${this.apiKey}`,
1278
- "content-type": "application/json"
1279
- },
1280
- body: JSON.stringify({
1281
- model: this.model,
1282
- query,
1283
- documents: candidates.map((c) => c.text),
1284
- top_k: candidates.length
1285
- }),
1286
- signal: signal ?? AbortSignal.timeout(timeoutMs)
1287
- });
1288
- } catch (e) {
1289
- if (e?.name === "TimeoutError" || e?.name === "AbortError") {
1290
- throw new Error(`Voyage rerank request timed out after ${timeoutMs}ms \u2014 is the API responsive?`);
1291
- }
1292
- throw e;
1293
- }
1294
- if (!res.ok) throw new Error(`Voyage rerank ${res.status}: ${await safeErrorBody4(res)}`);
1295
- const json = await res.json();
1296
- return json;
1297
- },
1298
- { maxAttempts: 3, signal }
1299
- );
1300
- this._usage.requests++;
1301
- if (result.usage?.total_tokens) {
1302
- this._usage.tokens += result.usage.total_tokens;
1303
- }
1304
- return result.data.map((d) => ({
1305
- id: candidates[d.index].id,
1306
- score: d.relevance_score
1307
- }));
1308
- }
1309
- };
1310
-
1311
- // src/providers/rerank/cohere.ts
1312
- var CohereRerankProvider = class {
1313
- name = "cohere";
1314
- apiKey;
1315
- model;
1316
- /** Accumulated usage across all rerank() calls on this instance. */
1317
- _usage = { tokens: 0, requests: 0 };
1318
- constructor(opts = {}) {
1319
- this.apiKey = opts.apiKey ?? process.env.COHERE_API_KEY ?? "";
1320
- this.model = opts.model ?? "rerank-v3.5";
1321
- if (!this.apiKey) {
1322
- throw new Error("cohere rerank provider requires COHERE_API_KEY");
1323
- }
1324
- }
1325
- /** Get accumulated usage stats. */
1326
- get usage() {
1327
- return { ...this._usage };
1328
- }
1329
- /** Reset accumulated usage counters. */
1330
- resetUsage() {
1331
- this._usage = { tokens: 0, requests: 0 };
1332
- }
1333
- async rerank(query, candidates, signal) {
1334
- if (candidates.length === 0) return [];
1335
- const timeoutMs = providerTimeoutMs4();
1336
- const { data: result } = await withRetry(
1337
- async () => {
1338
- let res;
1339
- try {
1340
- res = await fetch("https://api.cohere.com/v2/rerank", {
1341
- method: "POST",
1342
- headers: {
1343
- "authorization": `Bearer ${this.apiKey}`,
1344
- "content-type": "application/json"
1345
- },
1346
- body: JSON.stringify({
1347
- model: this.model,
1348
- query,
1349
- documents: candidates.map((c) => c.text),
1350
- top_n: candidates.length
1351
- }),
1352
- signal: signal ?? AbortSignal.timeout(timeoutMs)
1353
- });
1354
- } catch (e) {
1355
- if (e?.name === "TimeoutError" || e?.name === "AbortError") {
1356
- throw new Error(`Cohere rerank request timed out after ${timeoutMs}ms \u2014 is the API responsive?`);
1357
- }
1358
- throw e;
1359
- }
1360
- if (!res.ok) throw new Error(`Cohere rerank ${res.status}: ${await safeErrorBody4(res)}`);
1361
- const json = await res.json();
1362
- return json;
1363
- },
1364
- { maxAttempts: 3, signal }
1365
- );
1366
- this._usage.requests++;
1367
- if (result.meta?.billed_units?.search_units) {
1368
- this._usage.tokens += result.meta.billed_units.search_units;
1369
- }
1370
- return result.results.map((r) => ({
1371
- id: candidates[r.index].id,
1372
- score: r.relevance_score
1373
- }));
1374
- }
1375
- };
1376
-
1377
- // src/hybrid.ts
1378
- var MAX_LIMIT = 500;
1379
- function clampLimit(value, fallback, name) {
1380
- const v = value ?? fallback;
1381
- if (typeof v !== "number" || !Number.isFinite(v)) {
1382
- throw new Error(`hybridSearch: ${name} must be a finite number (got ${String(v)})`);
1383
- }
1384
- const n = v | 0;
1385
- return Math.max(1, Math.min(n, MAX_LIMIT));
1386
- }
1387
- function hasVecTable(db) {
1388
- const tagged = db;
1389
- if (typeof tagged.__synergyHasVec === "boolean") return tagged.__synergyHasVec;
1390
- try {
1391
- db.prepare("SELECT count(*) FROM chunks_vec LIMIT 0").get();
1392
- tagged.__synergyHasVec = true;
1393
- } catch {
1394
- tagged.__synergyHasVec = false;
1395
- }
1396
- return tagged.__synergyHasVec;
1397
- }
1398
- var warnedNoVec = false;
1399
- async function hybridSearch(db, query, opts = {}) {
1400
- const safeLimit = clampLimit(opts.limit, 20, "limit");
1401
- const safeTopK = clampLimit(opts.topK, 60, "topK");
1402
- const rerankRequested = clampLimit(opts.rerankCandidates, 20, "rerankCandidates");
1403
- const safeRerankN = Math.max(safeLimit, rerankRequested);
1404
- const rrfK = opts.rrfK ?? 60;
1405
- if (typeof rrfK !== "number" || !Number.isFinite(rrfK)) {
1406
- throw new Error(`hybridSearch: rrfK must be a finite number (got ${String(rrfK)})`);
1407
- }
1408
- const embedName = opts.embed ?? opts.embedProviderName ?? "ollama";
1409
- const dbDim = getEmbeddingDim(db) ?? void 0;
1410
- const emb = makeEmbeddingProvider(embedName, { dim: dbDim });
1411
- const filters = [];
1412
- const params = {};
1413
- if (opts.product) {
1414
- filters.push("ch.product = @product");
1415
- params.product = opts.product;
1416
- }
1417
- if (opts.since) {
1418
- filters.push("ch.released_at >= @since");
1419
- params.since = opts.since;
1420
- }
1421
- if (opts.until) {
1422
- filters.push("ch.released_at <= @until");
1423
- params.until = opts.until;
1424
- }
1425
- if (opts.kind) {
1426
- filters.push("EXISTS (SELECT 1 FROM changes c WHERE c.id = ch.change_id AND c.kind = @kind)");
1427
- params.kind = opts.kind;
1428
- }
1429
- const whereClause = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
1430
- let fts = [];
1431
- try {
1432
- const ftsSql = `
1433
- SELECT ch.id AS id, row_number() OVER (ORDER BY bm25(chunks_fts)) AS rank_pos
1434
- FROM chunks_fts
1435
- JOIN chunks ch ON chunks_fts.rowid = ch.id
1436
- WHERE chunks_fts MATCH @query
1437
- ${whereClause}
1438
- ORDER BY bm25(chunks_fts)
1439
- LIMIT @topK
1440
- `;
1441
- fts = db.prepare(ftsSql).all({ ...params, query, topK: safeTopK });
1442
- } catch (e) {
1443
- const sanitized = query.replace(/[^A-Za-z0-9 ]/g, " ").trim();
1444
- if (sanitized) {
1445
- const ftsSql = `
1446
- SELECT ch.id AS id, row_number() OVER (ORDER BY bm25(chunks_fts)) AS rank_pos
1447
- FROM chunks_fts
1448
- JOIN chunks ch ON chunks_fts.rowid = ch.id
1449
- WHERE chunks_fts MATCH @query
1450
- ${whereClause}
1451
- ORDER BY bm25(chunks_fts)
1452
- LIMIT @topK
1453
- `;
1454
- fts = db.prepare(ftsSql).all({ ...params, query: sanitized, topK: safeTopK });
1455
- }
1456
- }
1457
- const vecAvailable = hasVecTable(db);
1458
- let vec = [];
1459
- if (vecAvailable) {
1460
- const [qvec] = await emb.embed([query]);
1461
- const vecSql = `
1462
- SELECT ch.id AS id, row_number() OVER (ORDER BY distance) AS rank_pos
1463
- FROM chunks_vec
1464
- JOIN chunks ch ON ch.id = chunks_vec.rowid
1465
- WHERE chunks_vec.embedding MATCH @qvec AND k = @topK
1466
- ${whereClause}
1467
- ORDER BY distance
1468
- `;
1469
- vec = db.prepare(vecSql).all({ ...params, qvec, topK: safeTopK });
1470
- } else if (!warnedNoVec && !process.env.MCP_QUIET && !process.env.CLAUDE_SYNERGY_QUIET) {
1471
- console.warn("[hybrid] sqlite-vec table not found; using BM25-only");
1472
- warnedNoVec = true;
1473
- }
1474
- const scores = /* @__PURE__ */ new Map();
1475
- for (const r of fts) {
1476
- const cur = scores.get(r.id) ?? { bm25: null, vec: null, score: 0 };
1477
- cur.bm25 = r.rank_pos;
1478
- cur.score += 1 / (rrfK + r.rank_pos);
1479
- scores.set(r.id, cur);
1480
- }
1481
- for (const r of vec) {
1482
- const cur = scores.get(r.id) ?? { bm25: null, vec: null, score: 0 };
1483
- cur.vec = r.rank_pos;
1484
- cur.score += 1 / (rrfK + r.rank_pos);
1485
- scores.set(r.id, cur);
1486
- }
1487
- if (scores.size === 0) return [];
1488
- const rrfRanked = Array.from(scores.entries()).sort(([, a], [, b]) => b.score - a.score).slice(0, safeRerankN);
1489
- const candidateIds = rrfRanked.map(([id]) => id);
1490
- const placeholders = candidateIds.map(() => "?").join(",");
1491
- const rowsSql = `
1492
- SELECT ch.id AS chunk_id, ch.change_id, ch.product, ch.release_version AS version,
1493
- ch.released_at, c.kind, c.text, ch.contextualized
1494
- FROM chunks ch
1495
- JOIN changes c ON c.id = ch.change_id
1496
- WHERE ch.id IN (${placeholders})
1497
- `;
1498
- const rows = db.prepare(rowsSql).all(...candidateIds);
1499
- const byId = /* @__PURE__ */ new Map();
1500
- for (const r of rows) byId.set(r.chunk_id, r);
1501
- const rerankProviderName = opts.rerankProviderName ?? (isKnownReranker(opts.rerank) ? opts.rerank : "none");
1502
- let rerankScores = /* @__PURE__ */ new Map();
1503
- if (rerankProviderName !== "none") {
1504
- const reranker = makeRerankProvider(rerankProviderName);
1505
- const candidates = rrfRanked.slice(0, rerankRequested).map(([id]) => ({ id, text: byId.get(id)?.contextualized ?? byId.get(id)?.text ?? "" })).filter((c) => c.text);
1506
- const results = await reranker.rerank(query, candidates);
1507
- for (const r of results) rerankScores.set(r.id, r.score);
1508
- }
1509
- const final = rrfRanked.map(([id, score]) => ({
1510
- id,
1511
- rrf: score.score,
1512
- bm25: score.bm25,
1513
- vec: score.vec,
1514
- rerank: rerankScores.get(id) ?? null
1515
- })).sort((a, b) => {
1516
- if (rerankProviderName !== "none" && a.rerank !== null && b.rerank !== null) {
1517
- return b.rerank - a.rerank;
1518
- }
1519
- return b.rrf - a.rrf;
1520
- }).slice(0, safeLimit);
1521
- return final.map((entry) => {
1522
- const r = byId.get(entry.id);
1523
- if (!r) return null;
1524
- return {
1525
- ...r,
1526
- bm25_rank: entry.bm25,
1527
- vec_rank: entry.vec,
1528
- rrf_score: entry.rrf,
1529
- rerank_score: entry.rerank
1530
- };
1531
- }).filter(Boolean);
1532
- }
1533
- function isKnownReranker(v) {
1534
- return v === "none" || v === "ollama-judge" || v === "voyage" || v === "cohere";
1535
- }
1536
- function makeRerankProvider(name) {
1537
- switch (name) {
1538
- case "ollama-judge":
1539
- return new OllamaJudgeRerankProvider();
1540
- case "voyage":
1541
- return new VoyageRerankProvider();
1542
- case "cohere":
1543
- return new CohereRerankProvider();
1544
- }
1545
- }
1546
-
1547
864
  export {
1548
865
  AppError,
1549
866
  formatError,
1550
867
  openDb,
1551
868
  initSchema,
1552
- searchChanges,
1553
- lookupEntity,
1554
- recentReleases,
1555
- listProducts,
1556
- entityFrequency,
1557
- browseChanges,
1558
- getChangesSince,
1559
- compareVersions,
1560
- listSynergies,
1561
- getSynergy,
869
+ getEmbeddingDim,
870
+ withRetry,
871
+ providerTimeoutMs4 as providerTimeoutMs,
872
+ safeErrorBody4 as safeErrorBody,
873
+ initVecSchema,
1562
874
  embedAll,
1563
- hybridSearch
875
+ makeEmbeddingProvider
1564
876
  };