@tobilu/qmd 1.0.6 → 1.0.7
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 +23 -0
- package/dist/collections.js +15 -1
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +5 -0
- package/dist/qmd.js +84 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.0.7] - 2026-02-18
|
|
6
|
+
|
|
7
|
+
### Changes
|
|
8
|
+
|
|
9
|
+
- LLM: add LiquidAI LFM2-1.2B as an alternative base model for query
|
|
10
|
+
expansion fine-tuning. LFM2's hybrid architecture (convolutions + attention)
|
|
11
|
+
is 2x faster at decode/prefill vs standard transformers — good fit for
|
|
12
|
+
on-device inference.
|
|
13
|
+
- CLI: support multiple `-c` flags to search across several collections at
|
|
14
|
+
once (e.g. `qmd search -c notes -c journals "query"`). #191 (thanks
|
|
15
|
+
@openclaw)
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- Return empty JSON array `[]` instead of no output when `--json` search
|
|
20
|
+
finds no results.
|
|
21
|
+
- Resolve relative paths passed to `--index` so they don't produce malformed
|
|
22
|
+
config entries.
|
|
23
|
+
- Respect `XDG_CONFIG_HOME` for collection config path instead of always
|
|
24
|
+
using `~/.config`. #190 (thanks @openclaw)
|
|
25
|
+
- CLI: empty-collection hint now shows the correct `collection add` command.
|
|
26
|
+
#200 (thanks @vincentkoc)
|
|
27
|
+
|
|
5
28
|
## [1.0.6] - 2026-02-16
|
|
6
29
|
|
|
7
30
|
### Changes
|
package/dist/collections.js
CHANGED
|
@@ -18,13 +18,27 @@ let currentIndexName = "index";
|
|
|
18
18
|
* Config file will be ~/.config/qmd/{indexName}.yml
|
|
19
19
|
*/
|
|
20
20
|
export function setConfigIndexName(name) {
|
|
21
|
-
|
|
21
|
+
// Resolve relative paths to absolute paths and sanitize for use as filename
|
|
22
|
+
if (name.includes('/')) {
|
|
23
|
+
const { resolve } = require('path');
|
|
24
|
+
const { cwd } = require('process');
|
|
25
|
+
const absolutePath = resolve(cwd(), name);
|
|
26
|
+
// Replace path separators with underscores to create a valid filename
|
|
27
|
+
currentIndexName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
currentIndexName = name;
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
function getConfigDir() {
|
|
24
34
|
// Allow override via QMD_CONFIG_DIR for testing
|
|
25
35
|
if (process.env.QMD_CONFIG_DIR) {
|
|
26
36
|
return process.env.QMD_CONFIG_DIR;
|
|
27
37
|
}
|
|
38
|
+
// Respect XDG Base Directory specification (consistent with store.ts)
|
|
39
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
40
|
+
return join(process.env.XDG_CONFIG_HOME, "qmd");
|
|
41
|
+
}
|
|
28
42
|
return join(homedir(), ".config", "qmd");
|
|
29
43
|
}
|
|
30
44
|
function getConfigFilePath() {
|
package/dist/llm.d.ts
CHANGED
|
@@ -128,6 +128,8 @@ export type RerankDocument = {
|
|
|
128
128
|
text: string;
|
|
129
129
|
title?: string;
|
|
130
130
|
};
|
|
131
|
+
export declare const LFM2_GENERATE_MODEL = "hf:LiquidAI/LFM2-1.2B-GGUF/LFM2-1.2B-Q4_K_M.gguf";
|
|
132
|
+
export declare const LFM2_INSTRUCT_MODEL = "hf:LiquidAI/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf";
|
|
131
133
|
export declare const DEFAULT_EMBED_MODEL_URI = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
|
|
132
134
|
export declare const DEFAULT_RERANK_MODEL_URI = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
|
|
133
135
|
export declare const DEFAULT_GENERATE_MODEL_URI = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
|
package/dist/llm.js
CHANGED
|
@@ -33,6 +33,11 @@ const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma
|
|
|
33
33
|
const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
|
|
34
34
|
// const DEFAULT_GENERATE_MODEL = "hf:ggml-org/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf";
|
|
35
35
|
const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
|
|
36
|
+
// Alternative generation models for query expansion:
|
|
37
|
+
// LiquidAI LFM2 - hybrid architecture optimized for edge/on-device inference
|
|
38
|
+
// Use these as base for fine-tuning with configs/sft_lfm2.yaml
|
|
39
|
+
export const LFM2_GENERATE_MODEL = "hf:LiquidAI/LFM2-1.2B-GGUF/LFM2-1.2B-Q4_K_M.gguf";
|
|
40
|
+
export const LFM2_INSTRUCT_MODEL = "hf:LiquidAI/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf";
|
|
36
41
|
export const DEFAULT_EMBED_MODEL_URI = DEFAULT_EMBED_MODEL;
|
|
37
42
|
export const DEFAULT_RERANK_MODEL_URI = DEFAULT_RERANK_MODEL;
|
|
38
43
|
export const DEFAULT_GENERATE_MODEL_URI = DEFAULT_GENERATE_MODEL;
|
package/dist/qmd.js
CHANGED
|
@@ -36,7 +36,16 @@ function getDbPath() {
|
|
|
36
36
|
return store?.dbPath ?? storeDbPathOverride ?? getDefaultDbPath();
|
|
37
37
|
}
|
|
38
38
|
function setIndexName(name) {
|
|
39
|
-
|
|
39
|
+
let normalizedName = name;
|
|
40
|
+
// Normalize relative paths to prevent malformed database paths
|
|
41
|
+
if (name && name.includes('/')) {
|
|
42
|
+
const { resolve } = require('path');
|
|
43
|
+
const { cwd } = require('process');
|
|
44
|
+
const absolutePath = resolve(cwd(), name);
|
|
45
|
+
// Replace path separators with underscores to create a valid filename
|
|
46
|
+
normalizedName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
|
|
47
|
+
}
|
|
48
|
+
storeDbPathOverride = normalizedName ? getDefaultDbPath(normalizedName) : undefined;
|
|
40
49
|
// Reset open handle so next use opens the new index
|
|
41
50
|
closeDb();
|
|
42
51
|
}
|
|
@@ -962,7 +971,7 @@ function listFiles(pathArg) {
|
|
|
962
971
|
// No argument - list all collections
|
|
963
972
|
const yamlCollections = yamlListCollections();
|
|
964
973
|
if (yamlCollections.length === 0) {
|
|
965
|
-
console.log("No collections found. Run 'qmd add .' to index files.");
|
|
974
|
+
console.log("No collections found. Run 'qmd collection add .' to index files.");
|
|
966
975
|
closeDb();
|
|
967
976
|
return;
|
|
968
977
|
}
|
|
@@ -1086,7 +1095,7 @@ function collectionList() {
|
|
|
1086
1095
|
const db = getDb();
|
|
1087
1096
|
const collections = listCollections(db);
|
|
1088
1097
|
if (collections.length === 0) {
|
|
1089
|
-
console.log("No collections found. Run 'qmd add .' to create one.");
|
|
1098
|
+
console.log("No collections found. Run 'qmd collection add .' to create one.");
|
|
1090
1099
|
closeDb();
|
|
1091
1100
|
return;
|
|
1092
1101
|
}
|
|
@@ -1616,22 +1625,42 @@ function outputResults(results, query, opts) {
|
|
|
1616
1625
|
}
|
|
1617
1626
|
}
|
|
1618
1627
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1628
|
+
// Resolve -c collection filter: supports single string, array, or undefined.
|
|
1629
|
+
// Returns validated collection names (exits on unknown collection).
|
|
1630
|
+
function resolveCollectionFilter(raw) {
|
|
1631
|
+
if (!raw)
|
|
1632
|
+
return [];
|
|
1633
|
+
const names = Array.isArray(raw) ? raw : [raw];
|
|
1634
|
+
const validated = [];
|
|
1635
|
+
for (const name of names) {
|
|
1636
|
+
const coll = getCollectionFromYaml(name);
|
|
1625
1637
|
if (!coll) {
|
|
1626
|
-
console.error(`Collection not found: ${
|
|
1638
|
+
console.error(`Collection not found: ${name}`);
|
|
1627
1639
|
closeDb();
|
|
1628
1640
|
process.exit(1);
|
|
1629
1641
|
}
|
|
1630
|
-
|
|
1642
|
+
validated.push(name);
|
|
1631
1643
|
}
|
|
1644
|
+
return validated;
|
|
1645
|
+
}
|
|
1646
|
+
// Post-filter results to only include files from specified collections.
|
|
1647
|
+
function filterByCollections(results, collectionNames) {
|
|
1648
|
+
if (collectionNames.length <= 1)
|
|
1649
|
+
return results;
|
|
1650
|
+
const prefixes = collectionNames.map(n => `qmd://${n}/`);
|
|
1651
|
+
return results.filter(r => {
|
|
1652
|
+
const path = r.filepath || r.file || '';
|
|
1653
|
+
return prefixes.some(p => path.startsWith(p));
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function search(query, opts) {
|
|
1657
|
+
const db = getDb();
|
|
1658
|
+
// Validate collection filter (supports multiple -c flags)
|
|
1659
|
+
const collectionNames = resolveCollectionFilter(opts.collection);
|
|
1660
|
+
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
|
1632
1661
|
// Use large limit for --all, otherwise fetch more than needed and let outputResults filter
|
|
1633
1662
|
const fetchLimit = opts.all ? 100000 : Math.max(50, opts.limit * 2);
|
|
1634
|
-
const results = searchFTS(db, query, fetchLimit,
|
|
1663
|
+
const results = filterByCollections(searchFTS(db, query, fetchLimit, singleCollection), collectionNames);
|
|
1635
1664
|
// Add context to results
|
|
1636
1665
|
const resultsWithContext = results.map(r => ({
|
|
1637
1666
|
file: r.filepath,
|
|
@@ -1645,7 +1674,12 @@ function search(query, opts) {
|
|
|
1645
1674
|
}));
|
|
1646
1675
|
closeDb();
|
|
1647
1676
|
if (resultsWithContext.length === 0) {
|
|
1648
|
-
|
|
1677
|
+
if (opts.format === "json") {
|
|
1678
|
+
console.log("[]");
|
|
1679
|
+
}
|
|
1680
|
+
else {
|
|
1681
|
+
console.log("No results found.");
|
|
1682
|
+
}
|
|
1649
1683
|
return;
|
|
1650
1684
|
}
|
|
1651
1685
|
outputResults(resultsWithContext, query, opts);
|
|
@@ -1668,18 +1702,13 @@ function logExpansionTree(originalQuery, expanded) {
|
|
|
1668
1702
|
}
|
|
1669
1703
|
async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
1670
1704
|
const store = getStore();
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
console.error(`Collection not found: ${opts.collection}`);
|
|
1675
|
-
closeDb();
|
|
1676
|
-
process.exit(1);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1705
|
+
// Validate collection filter (supports multiple -c flags)
|
|
1706
|
+
const collectionNames = resolveCollectionFilter(opts.collection);
|
|
1707
|
+
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
|
1679
1708
|
checkIndexHealth(store.db);
|
|
1680
1709
|
await withLLMSession(async () => {
|
|
1681
|
-
|
|
1682
|
-
collection:
|
|
1710
|
+
let results = await vectorSearchQuery(store, query, {
|
|
1711
|
+
collection: singleCollection,
|
|
1683
1712
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1684
1713
|
minScore: opts.minScore || 0.3,
|
|
1685
1714
|
hooks: {
|
|
@@ -1689,9 +1718,21 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
|
1689
1718
|
},
|
|
1690
1719
|
},
|
|
1691
1720
|
});
|
|
1721
|
+
// Post-filter for multi-collection
|
|
1722
|
+
if (collectionNames.length > 1) {
|
|
1723
|
+
results = results.filter(r => {
|
|
1724
|
+
const prefixes = collectionNames.map(n => `qmd://${n}/`);
|
|
1725
|
+
return prefixes.some(p => r.file.startsWith(p));
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1692
1728
|
closeDb();
|
|
1693
1729
|
if (results.length === 0) {
|
|
1694
|
-
|
|
1730
|
+
if (opts.format === "json") {
|
|
1731
|
+
console.log("[]");
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
console.log("No results found.");
|
|
1735
|
+
}
|
|
1695
1736
|
return;
|
|
1696
1737
|
}
|
|
1697
1738
|
outputResults(results.map(r => ({
|
|
@@ -1707,18 +1748,13 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
|
1707
1748
|
}
|
|
1708
1749
|
async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rerankModel = DEFAULT_RERANK_MODEL) {
|
|
1709
1750
|
const store = getStore();
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
console.error(`Collection not found: ${opts.collection}`);
|
|
1714
|
-
closeDb();
|
|
1715
|
-
process.exit(1);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1751
|
+
// Validate collection filter (supports multiple -c flags)
|
|
1752
|
+
const collectionNames = resolveCollectionFilter(opts.collection);
|
|
1753
|
+
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
|
1718
1754
|
checkIndexHealth(store.db);
|
|
1719
1755
|
await withLLMSession(async () => {
|
|
1720
|
-
|
|
1721
|
-
collection:
|
|
1756
|
+
let results = await hybridQuery(store, query, {
|
|
1757
|
+
collection: singleCollection,
|
|
1722
1758
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1723
1759
|
minScore: opts.minScore || 0,
|
|
1724
1760
|
hooks: {
|
|
@@ -1738,9 +1774,21 @@ async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rera
|
|
|
1738
1774
|
},
|
|
1739
1775
|
},
|
|
1740
1776
|
});
|
|
1777
|
+
// Post-filter for multi-collection
|
|
1778
|
+
if (collectionNames.length > 1) {
|
|
1779
|
+
results = results.filter(r => {
|
|
1780
|
+
const prefixes = collectionNames.map(n => `qmd://${n}/`);
|
|
1781
|
+
return prefixes.some(p => r.file.startsWith(p));
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1741
1784
|
closeDb();
|
|
1742
1785
|
if (results.length === 0) {
|
|
1743
|
-
|
|
1786
|
+
if (opts.format === "json") {
|
|
1787
|
+
console.log("[]");
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
console.log("No results found.");
|
|
1791
|
+
}
|
|
1744
1792
|
return;
|
|
1745
1793
|
}
|
|
1746
1794
|
// Map to CLI output format — use bestChunk for snippet display
|
|
@@ -1768,9 +1816,6 @@ function parseCLI() {
|
|
|
1768
1816
|
context: {
|
|
1769
1817
|
type: "string",
|
|
1770
1818
|
},
|
|
1771
|
-
"no-lex": {
|
|
1772
|
-
type: "boolean",
|
|
1773
|
-
},
|
|
1774
1819
|
help: { type: "boolean", short: "h" },
|
|
1775
1820
|
version: { type: "boolean", short: "v" },
|
|
1776
1821
|
// Search options
|
|
@@ -1783,7 +1828,7 @@ function parseCLI() {
|
|
|
1783
1828
|
xml: { type: "boolean" },
|
|
1784
1829
|
files: { type: "boolean" },
|
|
1785
1830
|
json: { type: "boolean" },
|
|
1786
|
-
collection: { type: "string", short: "c" }, // Filter by collection
|
|
1831
|
+
collection: { type: "string", short: "c", multiple: true }, // Filter by collection(s)
|
|
1787
1832
|
// Collection options
|
|
1788
1833
|
name: { type: "string" }, // collection name
|
|
1789
1834
|
mask: { type: "string" }, // glob pattern
|