@ambicuity/kindx 0.1.0 → 1.1.0

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.
@@ -0,0 +1,243 @@
1
+ import chokidar from "chokidar";
2
+ import { relative as pathRelative } from "path";
3
+ import { realpathSync } from "fs";
4
+ const c = {
5
+ reset: "\x1b[0m",
6
+ bold: "\x1b[1m",
7
+ dim: "\x1b[2m",
8
+ red: "\x1b[31m",
9
+ green: "\x1b[32m",
10
+ yellow: "\x1b[33m",
11
+ cyan: "\x1b[36m",
12
+ };
13
+ function formatMs(ms) {
14
+ if (ms < 1000)
15
+ return `${ms}ms`;
16
+ return `${(ms / 1000).toFixed(1)}s`;
17
+ }
18
+ export class WatchDaemon {
19
+ store;
20
+ watchers = [];
21
+ eventQueue = [];
22
+ isProcessing = false;
23
+ debounceTimer = null;
24
+ debounceMs = 500;
25
+ // Track start time for freshness reporting
26
+ startTime = Date.now();
27
+ lastUpdateTs = Date.now();
28
+ eventCount = 0;
29
+ constructor(store) {
30
+ this.store = store;
31
+ }
32
+ /**
33
+ * Start watching specified collections (or all active if none specified)
34
+ */
35
+ async start(collectionNames) {
36
+ const { listCollections } = await import("./catalogs.js");
37
+ // Since we are reading from the DB/yaml config
38
+ const collections = listCollections();
39
+ // Filter if specific collections requested
40
+ const targetCollections = collectionNames && collectionNames.length > 0
41
+ ? collections.filter(c => collectionNames.includes(c.name))
42
+ : collections;
43
+ if (targetCollections.length === 0) {
44
+ console.log("No collections to watch.");
45
+ return;
46
+ }
47
+ try {
48
+ const { resolve } = await import("path");
49
+ const { homedir } = await import("os");
50
+ const { writeFileSync, existsSync, mkdirSync } = await import("fs");
51
+ const cacheDir = process.env.XDG_CACHE_HOME
52
+ ? resolve(process.env.XDG_CACHE_HOME, "kindx")
53
+ : resolve(homedir(), ".cache", "kindx");
54
+ if (!existsSync(cacheDir)) {
55
+ mkdirSync(cacheDir, { recursive: true });
56
+ }
57
+ writeFileSync(resolve(cacheDir, "watch.pid"), process.pid.toString(), "utf-8");
58
+ }
59
+ catch (err) {
60
+ console.error("Warning: could not write watch.pid file", err);
61
+ }
62
+ console.log(`Starting watcher daemon for ${targetCollections.length} collections...`);
63
+ const readyPromises = [];
64
+ for (const coll of targetCollections) {
65
+ // Build absolute watch path based on collection pwd and pattern
66
+ // chokidar handles globs like /path/to/collection/**/*.md
67
+ // We watch the whole directory and filter by pattern later to avoid chokidar glob complexity issues on some platforms
68
+ const watchTarget = coll.path;
69
+ const usePolling = process.env.CHOKIDAR_USEPOLLING === "1"
70
+ || process.env.CHOKIDAR_USEPOLLING === "true";
71
+ const pollInterval = Number(process.env.CHOKIDAR_INTERVAL || 100);
72
+ console.log(`- Watching [${c.bold}${coll.name}${c.reset}]: ${watchTarget} (pattern: ${coll.pattern})`);
73
+ const watcher = chokidar.watch(watchTarget, {
74
+ persistent: true,
75
+ ignoreInitial: true, // Don't trigger 'add' for existing files on startup
76
+ ignored: [/(^|[\/\\])\../, "**/node_modules/**", "dist/**"], // ignore dotfiles and common dirs
77
+ usePolling,
78
+ interval: pollInterval,
79
+ binaryInterval: pollInterval,
80
+ awaitWriteFinish: {
81
+ stabilityThreshold: 300,
82
+ pollInterval: 100
83
+ }
84
+ });
85
+ // Simple matcher for the collection pattern
86
+ const picomatchModule = await import("picomatch");
87
+ const picomatch = picomatchModule.default || picomatchModule;
88
+ const isMatch = picomatch(coll.pattern);
89
+ const processEvent = (type, absolutePath) => {
90
+ let canonicalPath = absolutePath;
91
+ try {
92
+ canonicalPath = realpathSync(absolutePath);
93
+ }
94
+ catch {
95
+ // The file may already be gone for unlink events; fall back to chokidar's path.
96
+ }
97
+ const relativePath = pathRelative(coll.path, canonicalPath).replace(/\\/g, "/");
98
+ if (relativePath === "" || relativePath === "." || relativePath.startsWith("../")) {
99
+ return;
100
+ }
101
+ // Check pattern
102
+ if (!isMatch(relativePath))
103
+ return;
104
+ this.enqueue(type, coll.name, relativePath, canonicalPath.replace(/\\/g, "/"));
105
+ };
106
+ watcher
107
+ .on("add", (path) => processEvent("add", path))
108
+ .on("change", (path) => processEvent("change", path))
109
+ .on("unlink", (path) => processEvent("unlink", path))
110
+ .on("error", (error) => console.error(`Watcher error for [${coll.name}]:`, error));
111
+ readyPromises.push(new Promise((resolve) => {
112
+ let settled = false;
113
+ watcher.once("ready", () => {
114
+ settled = true;
115
+ resolve();
116
+ });
117
+ watcher.once("error", () => {
118
+ if (!settled)
119
+ resolve();
120
+ });
121
+ }));
122
+ this.watchers.push(watcher);
123
+ }
124
+ await Promise.all(readyPromises);
125
+ console.log("Daemon active. Waiting for file system changes...");
126
+ }
127
+ /**
128
+ * Stop all watchers
129
+ */
130
+ async stop() {
131
+ console.log("Stopping watchers...");
132
+ await Promise.all(this.watchers.map(w => w.close()));
133
+ this.watchers = [];
134
+ if (this.debounceTimer)
135
+ clearTimeout(this.debounceTimer);
136
+ try {
137
+ const { resolve } = await import("path");
138
+ const { homedir } = await import("os");
139
+ const { unlinkSync, existsSync } = await import("fs");
140
+ const cacheDir = process.env.XDG_CACHE_HOME
141
+ ? resolve(process.env.XDG_CACHE_HOME, "kindx")
142
+ : resolve(homedir(), ".cache", "kindx");
143
+ const pidPath = resolve(cacheDir, "watch.pid");
144
+ if (existsSync(pidPath))
145
+ unlinkSync(pidPath);
146
+ }
147
+ catch (err) { }
148
+ }
149
+ /**
150
+ * Enqueue a filesystem event and trigger debounced processing
151
+ */
152
+ enqueue(type, collectionName, relativePath, absolutePath) {
153
+ // Deduplicate: if an event for this file already exists in queue, update it
154
+ const existingIdx = this.eventQueue.findIndex(e => e.collectionName === collectionName && e.relativePath === relativePath);
155
+ if (existingIdx >= 0) {
156
+ const existing = this.eventQueue[existingIdx];
157
+ // If we got 'add' then 'change', it's still an 'add' or 'change'.
158
+ // If we got 'add' then 'unlink', it's dead.
159
+ // Easiest approach: just overwrite with the latest state
160
+ if (type === "unlink") {
161
+ existing.type = "unlink";
162
+ }
163
+ else if (existing.type !== "add") {
164
+ existing.type = "change";
165
+ }
166
+ }
167
+ else {
168
+ this.eventQueue.push({ type, collectionName, relativePath, absolutePath });
169
+ }
170
+ // Debounce processing
171
+ if (this.debounceTimer)
172
+ clearTimeout(this.debounceTimer);
173
+ this.debounceTimer = setTimeout(() => {
174
+ this.processQueue().catch(err => {
175
+ console.error("Error processing watch queue:", err);
176
+ });
177
+ }, this.debounceMs);
178
+ }
179
+ /**
180
+ * Process queued events sequentially to prevent SQLite WAL contention
181
+ */
182
+ async processQueue() {
183
+ if (this.isProcessing || this.eventQueue.length === 0)
184
+ return;
185
+ this.isProcessing = true;
186
+ // Take a snapshot of the current queue
187
+ const batch = [...this.eventQueue];
188
+ this.eventQueue = [];
189
+ const startMs = Date.now();
190
+ let processed = 0;
191
+ // Format current time
192
+ const now = new Date().toLocaleTimeString();
193
+ console.log(`\n[${now}] Processing ${batch.length} changed files...`);
194
+ try {
195
+ // Need to cast store to any temporarily since the methods don't exist yet
196
+ const store = this.store;
197
+ for (const event of batch) {
198
+ try {
199
+ if (event.type === "unlink") {
200
+ const removed = await store.unlinkSingleFile(event.collectionName, event.relativePath);
201
+ if (removed) {
202
+ console.log(` ${c.red}[-]${c.reset} Removed: ${event.relativePath}`);
203
+ processed++;
204
+ }
205
+ }
206
+ else {
207
+ // add or change
208
+ const result = await store.indexSingleFile(event.collectionName, event.relativePath, event.absolutePath);
209
+ if (result === "embedded") {
210
+ console.log(` ${c.green}[+]${c.reset} Re-indexed: ${event.relativePath}`);
211
+ processed++;
212
+ }
213
+ else if (result === "unchanged") {
214
+ // hash matched, no change needed
215
+ // console.log(` [=] Unchanged: ${event.relativePath}`);
216
+ }
217
+ else if (result === "failed") {
218
+ console.log(` ${c.red}[x]${c.reset} Failed: ${event.relativePath}`);
219
+ }
220
+ }
221
+ }
222
+ catch (err) {
223
+ console.error(` ${c.red}[!]${c.reset} Error processing ${event.relativePath}:`, err);
224
+ }
225
+ }
226
+ if (processed > 0) {
227
+ this.lastUpdateTs = Date.now();
228
+ this.eventCount += processed;
229
+ const elapsed = Date.now() - startMs;
230
+ console.log(`[${new Date().toLocaleTimeString()}] Batch complete. ${processed} files updated in ${formatMs(elapsed)}`);
231
+ }
232
+ }
233
+ finally {
234
+ this.isProcessing = false;
235
+ // If more events arrived while we were processing, trigger again
236
+ if (this.eventQueue.length > 0) {
237
+ setTimeout(() => {
238
+ this.processQueue().catch(err => console.error(err));
239
+ }, 50);
240
+ }
241
+ }
242
+ }
243
+ }
package/package.json CHANGED
@@ -1,20 +1,32 @@
1
1
  {
2
2
  "name": "@ambicuity/kindx",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "KINDX - On-device hybrid search engine for markdown documents with BM25, vector embeddings, and LLM reranking",
5
5
  "type": "module",
6
+ "workspaces": [
7
+ "packages/*"
8
+ ],
6
9
  "bin": {
7
- "kindx": "dist/kindx.js"
10
+ "kindx": "bin/kindx"
8
11
  },
9
12
  "files": [
13
+ "bin/",
10
14
  "dist/",
15
+ "capabilities/kindx/",
11
16
  "LICENSE",
12
17
  "CHANGELOG.md"
13
18
  ],
14
19
  "scripts": {
15
20
  "prepare": "[ -d .git ] && ./tooling/install-hooks.sh || true",
16
21
  "build": "tsc -p tsconfig.build.json && printf '#!/usr/bin/env node\n' | cat - dist/kindx.js > dist/kindx.tmp && mv dist/kindx.tmp dist/kindx.js && chmod +x dist/kindx.js",
22
+ "build:packages": "npm run build -w @ambicuity/kindx-schemas && npm run build -w @ambicuity/kindx-client",
23
+ "build:all": "npm run build && npm run build:packages",
24
+ "pretest": "npm run build",
25
+ "pretest:all": "npm run build:all",
17
26
  "test": "vitest run --reporter=verbose specs/",
27
+ "test:packages": "npm run test -w @ambicuity/kindx-schemas && npm run test -w @ambicuity/kindx-client",
28
+ "test:python": "python3 -m unittest discover -s python/kindx-langchain/tests -v",
29
+ "test:all": "npm run pretest:all && npm run test && npm run test:packages && npm run test:python",
18
30
  "kindx": "tsx engine/kindx.ts",
19
31
  "index": "tsx engine/kindx.ts index",
20
32
  "vector": "tsx engine/kindx.ts vector",
@@ -25,7 +37,8 @@
25
37
  "release": "./tooling/release.sh"
26
38
  },
27
39
  "publishConfig": {
28
- "access": "public"
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
29
42
  },
30
43
  "repository": {
31
44
  "type": "git",
@@ -37,20 +50,21 @@
37
50
  },
38
51
  "dependencies": {
39
52
  "@modelcontextprotocol/sdk": "^1.25.1",
40
- "better-sqlite3": "^11.0.0",
53
+ "better-sqlite3": "^12.4.5",
54
+ "chokidar": "^5.0.0",
41
55
  "fast-glob": "^3.3.0",
42
56
  "node-llama-cpp": "^3.17.1",
43
57
  "picomatch": "^4.0.0",
44
- "sqlite-vec": "^0.1.7-alpha.2",
58
+ "sqlite-vec": "0.1.7",
45
59
  "yaml": "^2.8.2",
46
60
  "zod": "^4.2.1"
47
61
  },
48
62
  "optionalDependencies": {
49
- "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2",
50
- "sqlite-vec-darwin-x64": "^0.1.7-alpha.2",
51
- "sqlite-vec-linux-arm64": "^0.1.7-alpha.2",
52
- "sqlite-vec-linux-x64": "^0.1.7-alpha.2",
53
- "sqlite-vec-windows-x64": "^0.1.7-alpha.2"
63
+ "sqlite-vec-darwin-arm64": "0.1.7",
64
+ "sqlite-vec-darwin-x64": "0.1.7",
65
+ "sqlite-vec-linux-arm64": "0.1.7",
66
+ "sqlite-vec-linux-x64": "0.1.7",
67
+ "sqlite-vec-windows-x64": "0.1.7"
54
68
  },
55
69
  "devDependencies": {
56
70
  "@types/better-sqlite3": "^7.6.0",
@@ -58,7 +72,8 @@
58
72
  "@types/picomatch": "^4.0.2",
59
73
  "@types/yaml": "^1.9.6",
60
74
  "tsx": "^4.0.0",
61
- "vitest": "^3.0.0"
75
+ "typescript": "^5.9.3",
76
+ "vitest": "^4.1.2"
62
77
  },
63
78
  "peerDependencies": {
64
79
  "typescript": "^5.9.3"