@tobilu/qmd 1.1.1 → 1.1.2
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/README.md +29 -3
- package/dist/collections.d.ts +1 -0
- package/dist/llm.d.ts +19 -5
- package/dist/llm.js +98 -50
- package/dist/mcp.js +80 -9
- package/dist/qmd.js +103 -34
- package/dist/store.d.ts +44 -9
- package/dist/store.js +148 -16
- package/package.json +4 -3
package/dist/store.js
CHANGED
|
@@ -706,15 +706,24 @@ export function getDocid(hash) {
|
|
|
706
706
|
* - Preserve folder structure (a/b/c/d.md stays structured)
|
|
707
707
|
* - Preserve file extension
|
|
708
708
|
*/
|
|
709
|
+
/** Replace emoji/symbol codepoints with their hex representation (e.g. 🐘 → 1f418) */
|
|
710
|
+
function emojiToHex(str) {
|
|
711
|
+
return str.replace(/(?:\p{So}\p{Mn}?|\p{Sk})+/gu, (run) => {
|
|
712
|
+
// Split the run into individual emoji and convert each to hex, dash-separated
|
|
713
|
+
return [...run].filter(c => /\p{So}|\p{Sk}/u.test(c))
|
|
714
|
+
.map(c => c.codePointAt(0).toString(16)).join('-');
|
|
715
|
+
});
|
|
716
|
+
}
|
|
709
717
|
export function handelize(path) {
|
|
710
718
|
if (!path || path.trim() === '') {
|
|
711
719
|
throw new Error('handelize: path cannot be empty');
|
|
712
720
|
}
|
|
713
721
|
// Allow route-style "$" filenames while still rejecting paths with no usable content.
|
|
722
|
+
// Emoji (\p{So}) counts as valid content — they get converted to hex codepoints below.
|
|
714
723
|
const segments = path.split('/').filter(Boolean);
|
|
715
724
|
const lastSegment = segments[segments.length - 1] || '';
|
|
716
725
|
const filenameWithoutExt = lastSegment.replace(/\.[^.]+$/, '');
|
|
717
|
-
const hasValidContent = /[\p{L}\p{N}$]/u.test(filenameWithoutExt);
|
|
726
|
+
const hasValidContent = /[\p{L}\p{N}\p{So}\p{Sk}$]/u.test(filenameWithoutExt);
|
|
718
727
|
if (!hasValidContent) {
|
|
719
728
|
throw new Error(`handelize: path "${path}" has no valid filename content`);
|
|
720
729
|
}
|
|
@@ -724,6 +733,8 @@ export function handelize(path) {
|
|
|
724
733
|
.split('/')
|
|
725
734
|
.map((segment, idx, arr) => {
|
|
726
735
|
const isLastSegment = idx === arr.length - 1;
|
|
736
|
+
// Convert emoji to hex codepoints before cleaning
|
|
737
|
+
segment = emojiToHex(segment);
|
|
727
738
|
if (isLastSegment) {
|
|
728
739
|
// For the filename (last segment), preserve the extension
|
|
729
740
|
const extMatch = segment.match(/(\.[a-z0-9]+)$/i);
|
|
@@ -1745,7 +1756,7 @@ export async function searchVec(db, query, model, limit = 20, collectionName, se
|
|
|
1745
1756
|
// =============================================================================
|
|
1746
1757
|
async function getEmbedding(text, model, isQuery, session) {
|
|
1747
1758
|
// Format text using the appropriate prompt template
|
|
1748
|
-
const formattedText = isQuery ? formatQueryForEmbedding(text) : formatDocForEmbedding(text);
|
|
1759
|
+
const formattedText = isQuery ? formatQueryForEmbedding(text, model) : formatDocForEmbedding(text, undefined, model);
|
|
1749
1760
|
const result = session
|
|
1750
1761
|
? await session.embed(formattedText, { model, isQuery })
|
|
1751
1762
|
: await getDefaultLlamaCpp().embed(formattedText, { model, isQuery });
|
|
@@ -1817,35 +1828,40 @@ export async function expandQuery(query, model = DEFAULT_QUERY_MODEL, db) {
|
|
|
1817
1828
|
// =============================================================================
|
|
1818
1829
|
export async function rerank(query, documents, model = DEFAULT_RERANK_MODEL, db) {
|
|
1819
1830
|
const cachedResults = new Map();
|
|
1820
|
-
const
|
|
1831
|
+
const uncachedDocsByChunk = new Map();
|
|
1821
1832
|
// Check cache for each document
|
|
1822
1833
|
// Cache key includes chunk text — different queries can select different chunks
|
|
1823
1834
|
// from the same file, and the reranker score depends on which chunk was sent.
|
|
1835
|
+
// File path is excluded from the new cache key because the reranker score
|
|
1836
|
+
// depends on the chunk content, not where it came from.
|
|
1824
1837
|
for (const doc of documents) {
|
|
1825
|
-
const cacheKey = getCacheKey("rerank", { query,
|
|
1826
|
-
const
|
|
1838
|
+
const cacheKey = getCacheKey("rerank", { query, model, chunk: doc.text });
|
|
1839
|
+
const legacyCacheKey = getCacheKey("rerank", { query, file: doc.file, model, chunk: doc.text });
|
|
1840
|
+
const cached = getCachedResult(db, cacheKey) ?? getCachedResult(db, legacyCacheKey);
|
|
1827
1841
|
if (cached !== null) {
|
|
1828
|
-
cachedResults.set(doc.
|
|
1842
|
+
cachedResults.set(doc.text, parseFloat(cached));
|
|
1829
1843
|
}
|
|
1830
1844
|
else {
|
|
1831
|
-
|
|
1845
|
+
uncachedDocsByChunk.set(doc.text, { file: doc.file, text: doc.text });
|
|
1832
1846
|
}
|
|
1833
1847
|
}
|
|
1834
1848
|
// Rerank uncached documents using LlamaCpp
|
|
1835
|
-
if (
|
|
1849
|
+
if (uncachedDocsByChunk.size > 0) {
|
|
1836
1850
|
const llm = getDefaultLlamaCpp();
|
|
1851
|
+
const uncachedDocs = [...uncachedDocsByChunk.values()];
|
|
1837
1852
|
const rerankResult = await llm.rerank(query, uncachedDocs, { model });
|
|
1838
|
-
// Cache results
|
|
1839
|
-
const textByFile = new Map(
|
|
1853
|
+
// Cache results by chunk text so identical chunks across files are scored once.
|
|
1854
|
+
const textByFile = new Map(uncachedDocs.map(d => [d.file, d.text]));
|
|
1840
1855
|
for (const result of rerankResult.results) {
|
|
1841
|
-
const
|
|
1856
|
+
const chunk = textByFile.get(result.file) || "";
|
|
1857
|
+
const cacheKey = getCacheKey("rerank", { query, model, chunk });
|
|
1842
1858
|
setCachedResult(db, cacheKey, result.score.toString());
|
|
1843
|
-
cachedResults.set(
|
|
1859
|
+
cachedResults.set(chunk, result.score);
|
|
1844
1860
|
}
|
|
1845
1861
|
}
|
|
1846
1862
|
// Return all results sorted by score
|
|
1847
1863
|
return documents
|
|
1848
|
-
.map(doc => ({ file: doc.file, score: cachedResults.get(doc.
|
|
1864
|
+
.map(doc => ({ file: doc.file, score: cachedResults.get(doc.text) || 0 }))
|
|
1849
1865
|
.sort((a, b) => b.score - a.score);
|
|
1850
1866
|
}
|
|
1851
1867
|
// =============================================================================
|
|
@@ -1890,6 +1906,65 @@ export function reciprocalRankFusion(resultLists, weights = [], k = 60) {
|
|
|
1890
1906
|
.sort((a, b) => b.rrfScore - a.rrfScore)
|
|
1891
1907
|
.map(e => ({ ...e.result, score: e.rrfScore }));
|
|
1892
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Build per-document RRF contribution traces for explain/debug output.
|
|
1911
|
+
*/
|
|
1912
|
+
export function buildRrfTrace(resultLists, weights = [], listMeta = [], k = 60) {
|
|
1913
|
+
const traces = new Map();
|
|
1914
|
+
for (let listIdx = 0; listIdx < resultLists.length; listIdx++) {
|
|
1915
|
+
const list = resultLists[listIdx];
|
|
1916
|
+
if (!list)
|
|
1917
|
+
continue;
|
|
1918
|
+
const weight = weights[listIdx] ?? 1.0;
|
|
1919
|
+
const meta = listMeta[listIdx] ?? {
|
|
1920
|
+
source: "fts",
|
|
1921
|
+
queryType: "original",
|
|
1922
|
+
query: "",
|
|
1923
|
+
};
|
|
1924
|
+
for (let rank0 = 0; rank0 < list.length; rank0++) {
|
|
1925
|
+
const result = list[rank0];
|
|
1926
|
+
if (!result)
|
|
1927
|
+
continue;
|
|
1928
|
+
const rank = rank0 + 1; // 1-indexed rank for explain output
|
|
1929
|
+
const contribution = weight / (k + rank);
|
|
1930
|
+
const existing = traces.get(result.file);
|
|
1931
|
+
const detail = {
|
|
1932
|
+
listIndex: listIdx,
|
|
1933
|
+
source: meta.source,
|
|
1934
|
+
queryType: meta.queryType,
|
|
1935
|
+
query: meta.query,
|
|
1936
|
+
rank,
|
|
1937
|
+
weight,
|
|
1938
|
+
backendScore: result.score,
|
|
1939
|
+
rrfContribution: contribution,
|
|
1940
|
+
};
|
|
1941
|
+
if (existing) {
|
|
1942
|
+
existing.baseScore += contribution;
|
|
1943
|
+
existing.topRank = Math.min(existing.topRank, rank);
|
|
1944
|
+
existing.contributions.push(detail);
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
traces.set(result.file, {
|
|
1948
|
+
contributions: [detail],
|
|
1949
|
+
baseScore: contribution,
|
|
1950
|
+
topRank: rank,
|
|
1951
|
+
topRankBonus: 0,
|
|
1952
|
+
totalScore: 0,
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
for (const trace of traces.values()) {
|
|
1958
|
+
let bonus = 0;
|
|
1959
|
+
if (trace.topRank === 1)
|
|
1960
|
+
bonus = 0.05;
|
|
1961
|
+
else if (trace.topRank <= 3)
|
|
1962
|
+
bonus = 0.02;
|
|
1963
|
+
trace.topRankBonus = bonus;
|
|
1964
|
+
trace.totalScore = trace.baseScore + bonus;
|
|
1965
|
+
}
|
|
1966
|
+
return traces;
|
|
1967
|
+
}
|
|
1893
1968
|
/**
|
|
1894
1969
|
* Find a document by filename/path, docid (#hash), or with fuzzy matching.
|
|
1895
1970
|
* Returns document metadata without body by default.
|
|
@@ -2264,8 +2339,10 @@ export async function hybridQuery(store, query, options) {
|
|
|
2264
2339
|
const minScore = options?.minScore ?? 0;
|
|
2265
2340
|
const candidateLimit = options?.candidateLimit ?? RERANK_CANDIDATE_LIMIT;
|
|
2266
2341
|
const collection = options?.collection;
|
|
2342
|
+
const explain = options?.explain ?? false;
|
|
2267
2343
|
const hooks = options?.hooks;
|
|
2268
2344
|
const rankedLists = [];
|
|
2345
|
+
const rankedListMeta = [];
|
|
2269
2346
|
const docidMap = new Map(); // filepath -> docid
|
|
2270
2347
|
const hasVectors = !!store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
2271
2348
|
// Step 1: BM25 probe — strong signal skips expensive LLM expansion
|
|
@@ -2293,6 +2370,7 @@ export async function hybridQuery(store, query, options) {
|
|
|
2293
2370
|
file: r.filepath, displayPath: r.displayPath,
|
|
2294
2371
|
title: r.title, body: r.body || "", score: r.score,
|
|
2295
2372
|
})));
|
|
2373
|
+
rankedListMeta.push({ source: "fts", queryType: "original", query });
|
|
2296
2374
|
}
|
|
2297
2375
|
// Step 3: Route searches by query type
|
|
2298
2376
|
//
|
|
@@ -2310,17 +2388,18 @@ export async function hybridQuery(store, query, options) {
|
|
|
2310
2388
|
file: r.filepath, displayPath: r.displayPath,
|
|
2311
2389
|
title: r.title, body: r.body || "", score: r.score,
|
|
2312
2390
|
})));
|
|
2391
|
+
rankedListMeta.push({ source: "fts", queryType: "lex", query: q.text });
|
|
2313
2392
|
}
|
|
2314
2393
|
}
|
|
2315
2394
|
}
|
|
2316
2395
|
// 3b: Collect all texts that need vector search (original query + vec/hyde expansions)
|
|
2317
2396
|
if (hasVectors) {
|
|
2318
2397
|
const vecQueries = [
|
|
2319
|
-
{ text: query,
|
|
2398
|
+
{ text: query, queryType: "original" },
|
|
2320
2399
|
];
|
|
2321
2400
|
for (const q of expanded) {
|
|
2322
2401
|
if (q.type === 'vec' || q.type === 'hyde') {
|
|
2323
|
-
vecQueries.push({ text: q.text,
|
|
2402
|
+
vecQueries.push({ text: q.text, queryType: q.type });
|
|
2324
2403
|
}
|
|
2325
2404
|
}
|
|
2326
2405
|
// Batch embed all vector queries in a single call
|
|
@@ -2343,12 +2422,18 @@ export async function hybridQuery(store, query, options) {
|
|
|
2343
2422
|
file: r.filepath, displayPath: r.displayPath,
|
|
2344
2423
|
title: r.title, body: r.body || "", score: r.score,
|
|
2345
2424
|
})));
|
|
2425
|
+
rankedListMeta.push({
|
|
2426
|
+
source: "vec",
|
|
2427
|
+
queryType: vecQueries[i].queryType,
|
|
2428
|
+
query: vecQueries[i].text,
|
|
2429
|
+
});
|
|
2346
2430
|
}
|
|
2347
2431
|
}
|
|
2348
2432
|
}
|
|
2349
2433
|
// Step 4: RRF fusion — first 2 lists (original FTS + first vec) get 2x weight
|
|
2350
2434
|
const weights = rankedLists.map((_, i) => i < 2 ? 2.0 : 1.0);
|
|
2351
2435
|
const fused = reciprocalRankFusion(rankedLists, weights);
|
|
2436
|
+
const rrfTraceByFile = explain ? buildRrfTrace(rankedLists, weights, rankedListMeta) : null;
|
|
2352
2437
|
const candidates = fused.slice(0, candidateLimit);
|
|
2353
2438
|
if (candidates.length === 0)
|
|
2354
2439
|
return [];
|
|
@@ -2402,6 +2487,22 @@ export async function hybridQuery(store, query, options) {
|
|
|
2402
2487
|
const bestIdx = chunkInfo?.bestIdx ?? 0;
|
|
2403
2488
|
const bestChunk = chunkInfo?.chunks[bestIdx]?.text || candidate?.body || "";
|
|
2404
2489
|
const bestChunkPos = chunkInfo?.chunks[bestIdx]?.pos || 0;
|
|
2490
|
+
const trace = rrfTraceByFile?.get(r.file);
|
|
2491
|
+
const explainData = explain ? {
|
|
2492
|
+
ftsScores: trace?.contributions.filter(c => c.source === "fts").map(c => c.backendScore) ?? [],
|
|
2493
|
+
vectorScores: trace?.contributions.filter(c => c.source === "vec").map(c => c.backendScore) ?? [],
|
|
2494
|
+
rrf: {
|
|
2495
|
+
rank: rrfRank,
|
|
2496
|
+
positionScore: rrfScore,
|
|
2497
|
+
weight: rrfWeight,
|
|
2498
|
+
baseScore: trace?.baseScore ?? 0,
|
|
2499
|
+
topRankBonus: trace?.topRankBonus ?? 0,
|
|
2500
|
+
totalScore: trace?.totalScore ?? 0,
|
|
2501
|
+
contributions: trace?.contributions ?? [],
|
|
2502
|
+
},
|
|
2503
|
+
rerankScore: r.score,
|
|
2504
|
+
blendedScore,
|
|
2505
|
+
} : undefined;
|
|
2405
2506
|
return {
|
|
2406
2507
|
file: r.file,
|
|
2407
2508
|
displayPath: candidate?.displayPath || "",
|
|
@@ -2412,6 +2513,7 @@ export async function hybridQuery(store, query, options) {
|
|
|
2412
2513
|
score: blendedScore,
|
|
2413
2514
|
context: store.getContextForFile(r.file),
|
|
2414
2515
|
docid: docidMap.get(r.file) || "",
|
|
2516
|
+
...(explainData ? { explain: explainData } : {}),
|
|
2415
2517
|
};
|
|
2416
2518
|
}).sort((a, b) => b.score - a.score);
|
|
2417
2519
|
// Step 8: Dedup by file (safety net — prevents duplicate output)
|
|
@@ -2494,6 +2596,7 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2494
2596
|
const limit = options?.limit ?? 10;
|
|
2495
2597
|
const minScore = options?.minScore ?? 0;
|
|
2496
2598
|
const candidateLimit = options?.candidateLimit ?? RERANK_CANDIDATE_LIMIT;
|
|
2599
|
+
const explain = options?.explain ?? false;
|
|
2497
2600
|
const hooks = options?.hooks;
|
|
2498
2601
|
const collections = options?.collections;
|
|
2499
2602
|
if (searches.length === 0)
|
|
@@ -2518,6 +2621,7 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2518
2621
|
}
|
|
2519
2622
|
}
|
|
2520
2623
|
const rankedLists = [];
|
|
2624
|
+
const rankedListMeta = [];
|
|
2521
2625
|
const docidMap = new Map(); // filepath -> docid
|
|
2522
2626
|
const hasVectors = !!store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
2523
2627
|
// Helper to run search across collections (or all if undefined)
|
|
@@ -2534,13 +2638,18 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2534
2638
|
file: r.filepath, displayPath: r.displayPath,
|
|
2535
2639
|
title: r.title, body: r.body || "", score: r.score,
|
|
2536
2640
|
})));
|
|
2641
|
+
rankedListMeta.push({
|
|
2642
|
+
source: "fts",
|
|
2643
|
+
queryType: "lex",
|
|
2644
|
+
query: search.query,
|
|
2645
|
+
});
|
|
2537
2646
|
}
|
|
2538
2647
|
}
|
|
2539
2648
|
}
|
|
2540
2649
|
}
|
|
2541
2650
|
// Step 2: Batch embed and run vector searches for vec/hyde
|
|
2542
2651
|
if (hasVectors) {
|
|
2543
|
-
const vecSearches = searches.filter(s => s.type === 'vec' || s.type === 'hyde');
|
|
2652
|
+
const vecSearches = searches.filter((s) => s.type === 'vec' || s.type === 'hyde');
|
|
2544
2653
|
if (vecSearches.length > 0) {
|
|
2545
2654
|
const llm = getDefaultLlamaCpp();
|
|
2546
2655
|
const textsToEmbed = vecSearches.map(s => formatQueryForEmbedding(s.query));
|
|
@@ -2561,6 +2670,11 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2561
2670
|
file: r.filepath, displayPath: r.displayPath,
|
|
2562
2671
|
title: r.title, body: r.body || "", score: r.score,
|
|
2563
2672
|
})));
|
|
2673
|
+
rankedListMeta.push({
|
|
2674
|
+
source: "vec",
|
|
2675
|
+
queryType: vecSearches[i].type,
|
|
2676
|
+
query: vecSearches[i].query,
|
|
2677
|
+
});
|
|
2564
2678
|
}
|
|
2565
2679
|
}
|
|
2566
2680
|
}
|
|
@@ -2571,6 +2685,7 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2571
2685
|
// Step 3: RRF fusion — first list gets 2x weight (assume caller ordered by importance)
|
|
2572
2686
|
const weights = rankedLists.map((_, i) => i === 0 ? 2.0 : 1.0);
|
|
2573
2687
|
const fused = reciprocalRankFusion(rankedLists, weights);
|
|
2688
|
+
const rrfTraceByFile = explain ? buildRrfTrace(rankedLists, weights, rankedListMeta) : null;
|
|
2574
2689
|
const candidates = fused.slice(0, candidateLimit);
|
|
2575
2690
|
if (candidates.length === 0)
|
|
2576
2691
|
return [];
|
|
@@ -2627,6 +2742,22 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2627
2742
|
const bestIdx = chunkInfo?.bestIdx ?? 0;
|
|
2628
2743
|
const bestChunk = chunkInfo?.chunks[bestIdx]?.text || candidate?.body || "";
|
|
2629
2744
|
const bestChunkPos = chunkInfo?.chunks[bestIdx]?.pos || 0;
|
|
2745
|
+
const trace = rrfTraceByFile?.get(r.file);
|
|
2746
|
+
const explainData = explain ? {
|
|
2747
|
+
ftsScores: trace?.contributions.filter(c => c.source === "fts").map(c => c.backendScore) ?? [],
|
|
2748
|
+
vectorScores: trace?.contributions.filter(c => c.source === "vec").map(c => c.backendScore) ?? [],
|
|
2749
|
+
rrf: {
|
|
2750
|
+
rank: rrfRank,
|
|
2751
|
+
positionScore: rrfScore,
|
|
2752
|
+
weight: rrfWeight,
|
|
2753
|
+
baseScore: trace?.baseScore ?? 0,
|
|
2754
|
+
topRankBonus: trace?.topRankBonus ?? 0,
|
|
2755
|
+
totalScore: trace?.totalScore ?? 0,
|
|
2756
|
+
contributions: trace?.contributions ?? [],
|
|
2757
|
+
},
|
|
2758
|
+
rerankScore: r.score,
|
|
2759
|
+
blendedScore,
|
|
2760
|
+
} : undefined;
|
|
2630
2761
|
return {
|
|
2631
2762
|
file: r.file,
|
|
2632
2763
|
displayPath: candidate?.displayPath || "",
|
|
@@ -2637,6 +2768,7 @@ export async function structuredSearch(store, searches, options) {
|
|
|
2637
2768
|
score: blendedScore,
|
|
2638
2769
|
context: store.getContextForFile(r.file),
|
|
2639
2770
|
docid: docidMap.get(r.file) || "",
|
|
2771
|
+
...(explainData ? { explain: explainData } : {}),
|
|
2640
2772
|
};
|
|
2641
2773
|
}).sort((a, b) => b.score - a.score);
|
|
2642
2774
|
// Step 7: Dedup by file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobilu/qmd",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
40
40
|
"better-sqlite3": "^11.0.0",
|
|
41
41
|
"fast-glob": "^3.3.0",
|
|
42
|
-
"node-llama-cpp": "^3.
|
|
42
|
+
"node-llama-cpp": "^3.17.1",
|
|
43
43
|
"picomatch": "^4.0.0",
|
|
44
44
|
"sqlite-vec": "^0.1.7-alpha.2",
|
|
45
45
|
"yaml": "^2.8.2",
|
|
@@ -48,8 +48,9 @@
|
|
|
48
48
|
"optionalDependencies": {
|
|
49
49
|
"sqlite-vec-darwin-arm64": "^0.1.7-alpha.2",
|
|
50
50
|
"sqlite-vec-darwin-x64": "^0.1.7-alpha.2",
|
|
51
|
+
"sqlite-vec-linux-arm64": "^0.1.7-alpha.2",
|
|
51
52
|
"sqlite-vec-linux-x64": "^0.1.7-alpha.2",
|
|
52
|
-
"sqlite-vec-
|
|
53
|
+
"sqlite-vec-windows-x64": "^0.1.7-alpha.2"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/better-sqlite3": "^7.6.0",
|