@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 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
@@ -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
- currentIndexName = name;
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
- storeDbPathOverride = name ? getDefaultDbPath(name) : undefined;
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
- function search(query, opts) {
1620
- const db = getDb();
1621
- // Validate collection filter if specified
1622
- let collectionName;
1623
- if (opts.collection) {
1624
- const coll = getCollectionFromYaml(opts.collection);
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: ${opts.collection}`);
1638
+ console.error(`Collection not found: ${name}`);
1627
1639
  closeDb();
1628
1640
  process.exit(1);
1629
1641
  }
1630
- collectionName = opts.collection;
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, collectionName);
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
- console.log("No results found.");
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
- if (opts.collection) {
1672
- const coll = getCollectionFromYaml(opts.collection);
1673
- if (!coll) {
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
- const results = await vectorSearchQuery(store, query, {
1682
- collection: opts.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
- console.log("No results found.");
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
- if (opts.collection) {
1711
- const coll = getCollectionFromYaml(opts.collection);
1712
- if (!coll) {
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
- const results = await hybridQuery(store, query, {
1721
- collection: opts.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
- console.log("No results found.");
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tobilu/qmd",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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": {