@tobilu/qmd 1.0.5 → 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 +34 -0
- package/dist/collections.js +15 -1
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +5 -0
- package/dist/qmd.js +96 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
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
|
+
|
|
28
|
+
## [1.0.6] - 2026-02-16
|
|
29
|
+
|
|
30
|
+
### Changes
|
|
31
|
+
|
|
32
|
+
- CLI: `qmd status` now shows models with full HuggingFace links instead of
|
|
33
|
+
static names in `--help`. Model info is derived from the actual configured
|
|
34
|
+
URIs so it stays accurate if models change.
|
|
35
|
+
- Release tooling: pre-push hook handles non-interactive shells (CI, editors)
|
|
36
|
+
gracefully — warnings auto-proceed instead of hanging on a tty prompt.
|
|
37
|
+
Annotated tags now resolve correctly for CI checks.
|
|
38
|
+
|
|
5
39
|
## [1.0.5] - 2026-02-16
|
|
6
40
|
|
|
7
41
|
The npm package now ships compiled JavaScript instead of raw TypeScript,
|
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
|
}
|
|
@@ -256,6 +265,18 @@ async function showStatus() {
|
|
|
256
265
|
else {
|
|
257
266
|
console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`);
|
|
258
267
|
}
|
|
268
|
+
// Models
|
|
269
|
+
{
|
|
270
|
+
// hf:org/repo/file.gguf → https://huggingface.co/org/repo
|
|
271
|
+
const hfLink = (uri) => {
|
|
272
|
+
const match = uri.match(/^hf:([^/]+\/[^/]+)\//);
|
|
273
|
+
return match ? `https://huggingface.co/${match[1]}` : uri;
|
|
274
|
+
};
|
|
275
|
+
console.log(`\n${c.bold}Models${c.reset}`);
|
|
276
|
+
console.log(` Embedding: ${hfLink(DEFAULT_EMBED_MODEL_URI)}`);
|
|
277
|
+
console.log(` Reranking: ${hfLink(DEFAULT_RERANK_MODEL_URI)}`);
|
|
278
|
+
console.log(` Generation: ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`);
|
|
279
|
+
}
|
|
259
280
|
// Device / GPU info
|
|
260
281
|
try {
|
|
261
282
|
const llm = getDefaultLlamaCpp();
|
|
@@ -950,7 +971,7 @@ function listFiles(pathArg) {
|
|
|
950
971
|
// No argument - list all collections
|
|
951
972
|
const yamlCollections = yamlListCollections();
|
|
952
973
|
if (yamlCollections.length === 0) {
|
|
953
|
-
console.log("No collections found. Run 'qmd add .' to index files.");
|
|
974
|
+
console.log("No collections found. Run 'qmd collection add .' to index files.");
|
|
954
975
|
closeDb();
|
|
955
976
|
return;
|
|
956
977
|
}
|
|
@@ -1074,7 +1095,7 @@ function collectionList() {
|
|
|
1074
1095
|
const db = getDb();
|
|
1075
1096
|
const collections = listCollections(db);
|
|
1076
1097
|
if (collections.length === 0) {
|
|
1077
|
-
console.log("No collections found. Run 'qmd add .' to create one.");
|
|
1098
|
+
console.log("No collections found. Run 'qmd collection add .' to create one.");
|
|
1078
1099
|
closeDb();
|
|
1079
1100
|
return;
|
|
1080
1101
|
}
|
|
@@ -1604,22 +1625,42 @@ function outputResults(results, query, opts) {
|
|
|
1604
1625
|
}
|
|
1605
1626
|
}
|
|
1606
1627
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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);
|
|
1613
1637
|
if (!coll) {
|
|
1614
|
-
console.error(`Collection not found: ${
|
|
1638
|
+
console.error(`Collection not found: ${name}`);
|
|
1615
1639
|
closeDb();
|
|
1616
1640
|
process.exit(1);
|
|
1617
1641
|
}
|
|
1618
|
-
|
|
1642
|
+
validated.push(name);
|
|
1619
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;
|
|
1620
1661
|
// Use large limit for --all, otherwise fetch more than needed and let outputResults filter
|
|
1621
1662
|
const fetchLimit = opts.all ? 100000 : Math.max(50, opts.limit * 2);
|
|
1622
|
-
const results = searchFTS(db, query, fetchLimit,
|
|
1663
|
+
const results = filterByCollections(searchFTS(db, query, fetchLimit, singleCollection), collectionNames);
|
|
1623
1664
|
// Add context to results
|
|
1624
1665
|
const resultsWithContext = results.map(r => ({
|
|
1625
1666
|
file: r.filepath,
|
|
@@ -1633,7 +1674,12 @@ function search(query, opts) {
|
|
|
1633
1674
|
}));
|
|
1634
1675
|
closeDb();
|
|
1635
1676
|
if (resultsWithContext.length === 0) {
|
|
1636
|
-
|
|
1677
|
+
if (opts.format === "json") {
|
|
1678
|
+
console.log("[]");
|
|
1679
|
+
}
|
|
1680
|
+
else {
|
|
1681
|
+
console.log("No results found.");
|
|
1682
|
+
}
|
|
1637
1683
|
return;
|
|
1638
1684
|
}
|
|
1639
1685
|
outputResults(resultsWithContext, query, opts);
|
|
@@ -1656,18 +1702,13 @@ function logExpansionTree(originalQuery, expanded) {
|
|
|
1656
1702
|
}
|
|
1657
1703
|
async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
1658
1704
|
const store = getStore();
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
console.error(`Collection not found: ${opts.collection}`);
|
|
1663
|
-
closeDb();
|
|
1664
|
-
process.exit(1);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1705
|
+
// Validate collection filter (supports multiple -c flags)
|
|
1706
|
+
const collectionNames = resolveCollectionFilter(opts.collection);
|
|
1707
|
+
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
|
1667
1708
|
checkIndexHealth(store.db);
|
|
1668
1709
|
await withLLMSession(async () => {
|
|
1669
|
-
|
|
1670
|
-
collection:
|
|
1710
|
+
let results = await vectorSearchQuery(store, query, {
|
|
1711
|
+
collection: singleCollection,
|
|
1671
1712
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1672
1713
|
minScore: opts.minScore || 0.3,
|
|
1673
1714
|
hooks: {
|
|
@@ -1677,9 +1718,21 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
|
1677
1718
|
},
|
|
1678
1719
|
},
|
|
1679
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
|
+
}
|
|
1680
1728
|
closeDb();
|
|
1681
1729
|
if (results.length === 0) {
|
|
1682
|
-
|
|
1730
|
+
if (opts.format === "json") {
|
|
1731
|
+
console.log("[]");
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
console.log("No results found.");
|
|
1735
|
+
}
|
|
1683
1736
|
return;
|
|
1684
1737
|
}
|
|
1685
1738
|
outputResults(results.map(r => ({
|
|
@@ -1695,18 +1748,13 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
|
|
|
1695
1748
|
}
|
|
1696
1749
|
async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rerankModel = DEFAULT_RERANK_MODEL) {
|
|
1697
1750
|
const store = getStore();
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
console.error(`Collection not found: ${opts.collection}`);
|
|
1702
|
-
closeDb();
|
|
1703
|
-
process.exit(1);
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1751
|
+
// Validate collection filter (supports multiple -c flags)
|
|
1752
|
+
const collectionNames = resolveCollectionFilter(opts.collection);
|
|
1753
|
+
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
|
1706
1754
|
checkIndexHealth(store.db);
|
|
1707
1755
|
await withLLMSession(async () => {
|
|
1708
|
-
|
|
1709
|
-
collection:
|
|
1756
|
+
let results = await hybridQuery(store, query, {
|
|
1757
|
+
collection: singleCollection,
|
|
1710
1758
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1711
1759
|
minScore: opts.minScore || 0,
|
|
1712
1760
|
hooks: {
|
|
@@ -1726,9 +1774,21 @@ async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rera
|
|
|
1726
1774
|
},
|
|
1727
1775
|
},
|
|
1728
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
|
+
}
|
|
1729
1784
|
closeDb();
|
|
1730
1785
|
if (results.length === 0) {
|
|
1731
|
-
|
|
1786
|
+
if (opts.format === "json") {
|
|
1787
|
+
console.log("[]");
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
console.log("No results found.");
|
|
1791
|
+
}
|
|
1732
1792
|
return;
|
|
1733
1793
|
}
|
|
1734
1794
|
// Map to CLI output format — use bestChunk for snippet display
|
|
@@ -1756,9 +1816,6 @@ function parseCLI() {
|
|
|
1756
1816
|
context: {
|
|
1757
1817
|
type: "string",
|
|
1758
1818
|
},
|
|
1759
|
-
"no-lex": {
|
|
1760
|
-
type: "boolean",
|
|
1761
|
-
},
|
|
1762
1819
|
help: { type: "boolean", short: "h" },
|
|
1763
1820
|
version: { type: "boolean", short: "v" },
|
|
1764
1821
|
// Search options
|
|
@@ -1771,7 +1828,7 @@ function parseCLI() {
|
|
|
1771
1828
|
xml: { type: "boolean" },
|
|
1772
1829
|
files: { type: "boolean" },
|
|
1773
1830
|
json: { type: "boolean" },
|
|
1774
|
-
collection: { type: "string", short: "c" }, // Filter by collection
|
|
1831
|
+
collection: { type: "string", short: "c", multiple: true }, // Filter by collection(s)
|
|
1775
1832
|
// Collection options
|
|
1776
1833
|
name: { type: "string" }, // collection name
|
|
1777
1834
|
mask: { type: "string" }, // glob pattern
|
|
@@ -1877,11 +1934,6 @@ function showHelp() {
|
|
|
1877
1934
|
console.log(" --max-bytes <num> - Skip files larger than N bytes (default: 10240)");
|
|
1878
1935
|
console.log(" --json/--csv/--md/--xml/--files - Output format (same as search)");
|
|
1879
1936
|
console.log("");
|
|
1880
|
-
console.log("Models (auto-downloaded from HuggingFace):");
|
|
1881
|
-
console.log(" Embedding: embeddinggemma-300M-Q8_0");
|
|
1882
|
-
console.log(" Reranking: qwen3-reranker-0.6b-q8_0");
|
|
1883
|
-
console.log(" Generation: Qwen3-0.6B-Q8_0");
|
|
1884
|
-
console.log("");
|
|
1885
1937
|
console.log(`Index: ${getDbPath()}`);
|
|
1886
1938
|
}
|
|
1887
1939
|
async function showVersion() {
|