@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.
- package/CHANGELOG.md +51 -0
- package/README.md +409 -129
- package/bin/kindx +38 -0
- package/capabilities/kindx/SKILL.md +127 -0
- package/capabilities/kindx/references/mcp-setup.md +102 -0
- package/dist/catalogs.js +57 -16
- package/dist/inference.d.ts +82 -7
- package/dist/inference.js +241 -49
- package/dist/kindx.js +425 -91
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +133 -0
- package/dist/protocol.d.ts +2 -1
- package/dist/protocol.js +110 -6
- package/dist/remote-llm.d.ts +23 -0
- package/dist/remote-llm.js +307 -0
- package/dist/repository.d.ts +18 -1
- package/dist/repository.js +260 -35
- package/dist/watcher.d.ts +29 -0
- package/dist/watcher.js +243 -0
- package/package.json +26 -11
package/dist/watcher.js
ADDED
|
@@ -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": "
|
|
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": "
|
|
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": "^
|
|
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": "
|
|
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": "
|
|
50
|
-
"sqlite-vec-darwin-x64": "
|
|
51
|
-
"sqlite-vec-linux-arm64": "
|
|
52
|
-
"sqlite-vec-linux-x64": "
|
|
53
|
-
"sqlite-vec-windows-x64": "
|
|
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
|
-
"
|
|
75
|
+
"typescript": "^5.9.3",
|
|
76
|
+
"vitest": "^4.1.2"
|
|
62
77
|
},
|
|
63
78
|
"peerDependencies": {
|
|
64
79
|
"typescript": "^5.9.3"
|