@tobilu/qmd 1.1.5 → 1.1.6

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,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.6] - 2026-03-09
6
+
7
+ QMD can now be used as a library. `import { createStore } from '@tobilu/qmd'`
8
+ gives you the full search and indexing API — hybrid query, BM25, structured
9
+ search, collection/context management — without shelling out to the CLI.
10
+
11
+ ### Changes
12
+
13
+ - **SDK / library mode**: `createStore({ dbPath, config })` returns a
14
+ `QMDStore` with `query()`, `search()`, `structuredSearch()`, `get()`,
15
+ `multiGet()`, and collection/context management methods. Supports inline
16
+ config (no files needed) or a YAML config path.
17
+ - **Package exports**: `package.json` now declares `main`, `types`, and
18
+ `exports` so bundlers and TypeScript resolve `@tobilu/qmd` correctly.
19
+
5
20
  ## [1.1.5] - 2026-03-07
6
21
 
7
22
  Ambiguous queries like "performance" now produce dramatically better results
package/README.md CHANGED
@@ -137,6 +137,81 @@ LLM models stay loaded in VRAM across requests. Embedding/reranking contexts are
137
137
 
138
138
  Point any MCP client at `http://localhost:8181/mcp` to connect.
139
139
 
140
+ ### SDK / Library Usage
141
+
142
+ Use QMD as a library in your own Node.js or Bun applications:
143
+
144
+ ```sh
145
+ npm install @tobilu/qmd
146
+ ```
147
+
148
+ ```typescript
149
+ import { createStore } from '@tobilu/qmd'
150
+
151
+ // Create a store with inline config (no config file needed)
152
+ const store = createStore({
153
+ dbPath: './my-index.sqlite',
154
+ config: {
155
+ collections: {
156
+ docs: { path: '/path/to/docs', pattern: '**/*.md' },
157
+ notes: { path: '/path/to/notes', pattern: '**/*.md' },
158
+ },
159
+ },
160
+ })
161
+
162
+ // Or reference a YAML config file
163
+ const store2 = createStore({
164
+ dbPath: './my-index.sqlite',
165
+ configPath: './qmd.yml',
166
+ })
167
+ ```
168
+
169
+ **Search & retrieval:**
170
+
171
+ ```typescript
172
+ // Hybrid search: BM25 + vector + query expansion + LLM reranking (best quality)
173
+ const results = await store.query("authentication flow", { limit: 5 })
174
+
175
+ // Fast BM25 keyword search (no LLM, synchronous)
176
+ const keywords = store.search("auth middleware", { limit: 10 })
177
+
178
+ // Structured search with pre-expanded queries (for LLM callers)
179
+ const structured = await store.structuredSearch([
180
+ { type: 'lex', query: 'authentication' },
181
+ { type: 'vec', query: 'how users log in' },
182
+ ], { limit: 5 })
183
+
184
+ // Get a document by path or docid
185
+ const doc = store.get("docs/readme.md")
186
+ const byId = store.get("#abc123")
187
+
188
+ // Get multiple documents by glob
189
+ const { docs, errors } = store.multiGet("docs/**/*.md")
190
+ ```
191
+
192
+ **Collection & context management:**
193
+
194
+ ```typescript
195
+ // Add a collection
196
+ store.addCollection("myapp", { path: "/src/myapp", pattern: "**/*.ts" })
197
+
198
+ // Add context (improves search relevance)
199
+ store.addContext("myapp", "/auth", "Authentication and session management")
200
+ store.setGlobalContext("Internal engineering documentation")
201
+
202
+ // List everything
203
+ store.listCollections()
204
+ store.listContexts()
205
+ ```
206
+
207
+ **Lifecycle:**
208
+
209
+ ```typescript
210
+ store.close()
211
+ ```
212
+
213
+ The SDK requires explicit `dbPath` and config — no defaults are assumed. This makes it safe to embed in any application without side effects.
214
+
140
215
  ## Architecture
141
216
 
142
217
  ```
@@ -34,18 +34,32 @@ export interface CollectionConfig {
34
34
  export interface NamedCollection extends Collection {
35
35
  name: string;
36
36
  }
37
+ /**
38
+ * Set the config source for SDK mode.
39
+ * - File path: load/save from a specific YAML file
40
+ * - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
41
+ * - undefined: reset to default file-based config
42
+ */
43
+ export declare function setConfigSource(source?: {
44
+ configPath?: string;
45
+ config?: CollectionConfig;
46
+ }): void;
37
47
  /**
38
48
  * Set the current index name for config file lookup
39
49
  * Config file will be ~/.config/qmd/{indexName}.yml
40
50
  */
41
51
  export declare function setConfigIndexName(name: string): void;
42
52
  /**
43
- * Load configuration from ~/.config/qmd/index.yml
53
+ * Load configuration from the configured source.
54
+ * - Inline config: returns the in-memory object directly
55
+ * - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
44
56
  * Returns empty config if file doesn't exist
45
57
  */
46
58
  export declare function loadConfig(): CollectionConfig;
47
59
  /**
48
- * Save configuration to ~/.config/qmd/index.yml
60
+ * Save configuration to the configured source.
61
+ * - Inline config: updates the in-memory object (no file I/O)
62
+ * - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
49
63
  */
50
64
  export declare function saveConfig(config: CollectionConfig): void;
51
65
  /**
@@ -5,7 +5,7 @@
5
5
  * Collections define which directories to index and their associated contexts.
6
6
  */
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
- import { join } from "path";
8
+ import { join, dirname } from "path";
9
9
  import { homedir } from "os";
10
10
  import YAML from "yaml";
11
11
  // ============================================================================
@@ -13,6 +13,33 @@ import YAML from "yaml";
13
13
  // ============================================================================
14
14
  // Current index name (default: "index")
15
15
  let currentIndexName = "index";
16
+ // SDK mode: optional in-memory config or custom config path
17
+ let configSource = { type: 'file' };
18
+ /**
19
+ * Set the config source for SDK mode.
20
+ * - File path: load/save from a specific YAML file
21
+ * - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
22
+ * - undefined: reset to default file-based config
23
+ */
24
+ export function setConfigSource(source) {
25
+ if (!source) {
26
+ configSource = { type: 'file' };
27
+ return;
28
+ }
29
+ if (source.config) {
30
+ // Ensure collections object exists
31
+ if (!source.config.collections) {
32
+ source.config.collections = {};
33
+ }
34
+ configSource = { type: 'inline', config: source.config };
35
+ }
36
+ else if (source.configPath) {
37
+ configSource = { type: 'file', path: source.configPath };
38
+ }
39
+ else {
40
+ configSource = { type: 'file' };
41
+ }
42
+ }
16
43
  /**
17
44
  * Set the current index name for config file lookup
18
45
  * Config file will be ~/.config/qmd/{indexName}.yml
@@ -57,11 +84,18 @@ function ensureConfigDir() {
57
84
  // Core functions
58
85
  // ============================================================================
59
86
  /**
60
- * Load configuration from ~/.config/qmd/index.yml
87
+ * Load configuration from the configured source.
88
+ * - Inline config: returns the in-memory object directly
89
+ * - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
61
90
  * Returns empty config if file doesn't exist
62
91
  */
63
92
  export function loadConfig() {
64
- const configPath = getConfigFilePath();
93
+ // SDK inline config mode
94
+ if (configSource.type === 'inline') {
95
+ return configSource.config;
96
+ }
97
+ // File-based config (SDK custom path or default)
98
+ const configPath = configSource.path || getConfigFilePath();
65
99
  if (!existsSync(configPath)) {
66
100
  return { collections: {} };
67
101
  }
@@ -79,11 +113,21 @@ export function loadConfig() {
79
113
  }
80
114
  }
81
115
  /**
82
- * Save configuration to ~/.config/qmd/index.yml
116
+ * Save configuration to the configured source.
117
+ * - Inline config: updates the in-memory object (no file I/O)
118
+ * - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
83
119
  */
84
120
  export function saveConfig(config) {
85
- ensureConfigDir();
86
- const configPath = getConfigFilePath();
121
+ // SDK inline config mode: update in place, no file I/O
122
+ if (configSource.type === 'inline') {
123
+ configSource.config = config;
124
+ return;
125
+ }
126
+ const configPath = configSource.path || getConfigFilePath();
127
+ const configDir = dirname(configPath);
128
+ if (!existsSync(configDir)) {
129
+ mkdirSync(configDir, { recursive: true });
130
+ }
87
131
  try {
88
132
  const yaml = YAML.stringify(config, {
89
133
  indent: 2,
@@ -318,13 +362,18 @@ export function findContextForPath(collectionName, filePath) {
318
362
  * Get the config file path (useful for error messages)
319
363
  */
320
364
  export function getConfigPath() {
321
- return getConfigFilePath();
365
+ if (configSource.type === 'inline')
366
+ return '<inline>';
367
+ return configSource.path || getConfigFilePath();
322
368
  }
323
369
  /**
324
370
  * Check if config file exists
325
371
  */
326
372
  export function configExists() {
327
- return existsSync(getConfigFilePath());
373
+ if (configSource.type === 'inline')
374
+ return true;
375
+ const path = configSource.path || getConfigFilePath();
376
+ return existsSync(path);
328
377
  }
329
378
  /**
330
379
  * Validate a collection name
@@ -0,0 +1,129 @@
1
+ /**
2
+ * QMD SDK - Library mode for programmatic access to QMD search and indexing.
3
+ *
4
+ * Usage:
5
+ * import { createStore } from '@tobilu/qmd'
6
+ *
7
+ * const store = createStore({
8
+ * dbPath: './my-index.sqlite',
9
+ * config: {
10
+ * collections: {
11
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
12
+ * }
13
+ * }
14
+ * })
15
+ *
16
+ * const results = await store.query("how does auth work?")
17
+ * store.close()
18
+ */
19
+ import { type Store as InternalStore, type DocumentResult, type DocumentNotFound, type SearchResult, type HybridQueryResult, type HybridQueryOptions, type HybridQueryExplain, type StructuredSubSearch, type StructuredSearchOptions, type MultiGetResult, type IndexStatus, type IndexHealthInfo, type ExpandedQuery, type SearchHooks } from "./store.js";
20
+ import { type Collection, type CollectionConfig, type NamedCollection, type ContextMap } from "./collections.js";
21
+ export type { DocumentResult, DocumentNotFound, SearchResult, HybridQueryResult, HybridQueryOptions, HybridQueryExplain, StructuredSubSearch, StructuredSearchOptions, MultiGetResult, IndexStatus, IndexHealthInfo, ExpandedQuery, SearchHooks, Collection, CollectionConfig, NamedCollection, ContextMap, };
22
+ /**
23
+ * Options for creating a QMD store.
24
+ * You must provide `dbPath` and either `configPath` (YAML file) or `config` (inline).
25
+ */
26
+ export interface StoreOptions {
27
+ /** Path to the SQLite database file */
28
+ dbPath: string;
29
+ /** Path to a YAML config file (mutually exclusive with `config`) */
30
+ configPath?: string;
31
+ /** Inline collection config (mutually exclusive with `configPath`) */
32
+ config?: CollectionConfig;
33
+ }
34
+ /**
35
+ * The QMD SDK store — provides search, retrieval, collection management,
36
+ * context management, and indexing operations.
37
+ */
38
+ export interface QMDStore {
39
+ /** The underlying internal store (for advanced use) */
40
+ readonly internal: InternalStore;
41
+ /** Path to the SQLite database */
42
+ readonly dbPath: string;
43
+ /** Hybrid search: BM25 + vector + query expansion + LLM reranking */
44
+ query(query: string, options?: HybridQueryOptions): Promise<HybridQueryResult[]>;
45
+ /** BM25 full-text keyword search (fast, no LLM) */
46
+ search(query: string, options?: {
47
+ limit?: number;
48
+ collection?: string;
49
+ }): SearchResult[];
50
+ /** Structured search with pre-expanded queries (for LLM callers) */
51
+ structuredSearch(searches: StructuredSubSearch[], options?: StructuredSearchOptions): Promise<HybridQueryResult[]>;
52
+ /** Get a single document by path or docid */
53
+ get(pathOrDocid: string, options?: {
54
+ includeBody?: boolean;
55
+ }): DocumentResult | DocumentNotFound;
56
+ /** Get multiple documents by glob pattern or comma-separated list */
57
+ multiGet(pattern: string, options?: {
58
+ includeBody?: boolean;
59
+ maxBytes?: number;
60
+ }): {
61
+ docs: MultiGetResult[];
62
+ errors: string[];
63
+ };
64
+ /** Add or update a collection */
65
+ addCollection(name: string, opts: {
66
+ path: string;
67
+ pattern?: string;
68
+ ignore?: string[];
69
+ }): void;
70
+ /** Remove a collection */
71
+ removeCollection(name: string): boolean;
72
+ /** Rename a collection */
73
+ renameCollection(oldName: string, newName: string): boolean;
74
+ /** List all collections with document stats */
75
+ listCollections(): {
76
+ name: string;
77
+ pwd: string;
78
+ glob_pattern: string;
79
+ doc_count: number;
80
+ active_count: number;
81
+ last_modified: string | null;
82
+ }[];
83
+ /** Add context for a path within a collection */
84
+ addContext(collectionName: string, pathPrefix: string, contextText: string): boolean;
85
+ /** Remove context from a collection path */
86
+ removeContext(collectionName: string, pathPrefix: string): boolean;
87
+ /** Set global context (applies to all collections) */
88
+ setGlobalContext(context: string | undefined): void;
89
+ /** Get global context */
90
+ getGlobalContext(): string | undefined;
91
+ /** List all contexts across all collections */
92
+ listContexts(): Array<{
93
+ collection: string;
94
+ path: string;
95
+ context: string;
96
+ }>;
97
+ /** Get index status (document counts, collections, embedding state) */
98
+ getStatus(): IndexStatus;
99
+ /** Get index health info (stale embeddings, etc.) */
100
+ getIndexHealth(): IndexHealthInfo;
101
+ /** Close the database connection */
102
+ close(): void;
103
+ }
104
+ /**
105
+ * Create a QMD store for programmatic access to search and indexing.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * // With a YAML config file
110
+ * const store = createStore({
111
+ * dbPath: './index.sqlite',
112
+ * configPath: './qmd.yml',
113
+ * })
114
+ *
115
+ * // With inline config (no files needed besides the DB)
116
+ * const store = createStore({
117
+ * dbPath: './index.sqlite',
118
+ * config: {
119
+ * collections: {
120
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
121
+ * }
122
+ * }
123
+ * })
124
+ *
125
+ * const results = await store.query("authentication flow")
126
+ * store.close()
127
+ * ```
128
+ */
129
+ export declare function createStore(options: StoreOptions): QMDStore;
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * QMD SDK - Library mode for programmatic access to QMD search and indexing.
3
+ *
4
+ * Usage:
5
+ * import { createStore } from '@tobilu/qmd'
6
+ *
7
+ * const store = createStore({
8
+ * dbPath: './my-index.sqlite',
9
+ * config: {
10
+ * collections: {
11
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
12
+ * }
13
+ * }
14
+ * })
15
+ *
16
+ * const results = await store.query("how does auth work?")
17
+ * store.close()
18
+ */
19
+ import { createStore as createStoreInternal, hybridQuery, structuredSearch, listCollections as storeListCollections, } from "./store.js";
20
+ import { setConfigSource, loadConfig, addCollection as collectionsAddCollection, removeCollection as collectionsRemoveCollection, renameCollection as collectionsRenameCollection, listCollections as collectionsListCollections, addContext as collectionsAddContext, removeContext as collectionsRemoveContext, setGlobalContext as collectionsSetGlobalContext, getGlobalContext as collectionsGetGlobalContext, listAllContexts as collectionsListAllContexts, } from "./collections.js";
21
+ /**
22
+ * Create a QMD store for programmatic access to search and indexing.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // With a YAML config file
27
+ * const store = createStore({
28
+ * dbPath: './index.sqlite',
29
+ * configPath: './qmd.yml',
30
+ * })
31
+ *
32
+ * // With inline config (no files needed besides the DB)
33
+ * const store = createStore({
34
+ * dbPath: './index.sqlite',
35
+ * config: {
36
+ * collections: {
37
+ * docs: { path: '/path/to/docs', pattern: '**\/*.md' }
38
+ * }
39
+ * }
40
+ * })
41
+ *
42
+ * const results = await store.query("authentication flow")
43
+ * store.close()
44
+ * ```
45
+ */
46
+ export function createStore(options) {
47
+ if (!options.dbPath) {
48
+ throw new Error("dbPath is required");
49
+ }
50
+ if (!options.configPath && !options.config) {
51
+ throw new Error("Either configPath or config is required");
52
+ }
53
+ if (options.configPath && options.config) {
54
+ throw new Error("Provide either configPath or config, not both");
55
+ }
56
+ // Inject config source into collections module
57
+ setConfigSource({
58
+ configPath: options.configPath,
59
+ config: options.config,
60
+ });
61
+ // Create the internal store
62
+ const internal = createStoreInternal(options.dbPath);
63
+ const store = {
64
+ internal,
65
+ dbPath: internal.dbPath,
66
+ // Search & Retrieval
67
+ query: (q, opts) => hybridQuery(internal, q, opts),
68
+ search: (q, opts) => internal.searchFTS(q, opts?.limit, opts?.collection),
69
+ structuredSearch: (searches, opts) => structuredSearch(internal, searches, opts),
70
+ get: (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
71
+ multiGet: (pattern, opts) => internal.findDocuments(pattern, opts),
72
+ // Collection Management
73
+ addCollection: (name, opts) => {
74
+ collectionsAddCollection(name, opts.path, opts.pattern);
75
+ },
76
+ removeCollection: (name) => collectionsRemoveCollection(name),
77
+ renameCollection: (oldName, newName) => collectionsRenameCollection(oldName, newName),
78
+ listCollections: () => storeListCollections(internal.db),
79
+ // Context Management
80
+ addContext: (collectionName, pathPrefix, contextText) => collectionsAddContext(collectionName, pathPrefix, contextText),
81
+ removeContext: (collectionName, pathPrefix) => collectionsRemoveContext(collectionName, pathPrefix),
82
+ setGlobalContext: (context) => collectionsSetGlobalContext(context),
83
+ getGlobalContext: () => collectionsGetGlobalContext(),
84
+ listContexts: () => collectionsListAllContexts(),
85
+ // Index Health
86
+ getStatus: () => internal.getStatus(),
87
+ getIndexHealth: () => internal.getIndexHealth(),
88
+ // Lifecycle
89
+ close: () => {
90
+ internal.close();
91
+ setConfigSource(undefined); // Reset config source
92
+ },
93
+ };
94
+ return store;
95
+ }
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@tobilu/qmd",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
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
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
6
14
  "bin": {
7
15
  "qmd": "dist/qmd.js"
8
16
  },