indra_db_mcp 0.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/README.md +225 -0
- package/package.json +52 -0
- package/src/index.ts +529 -0
- package/src/indra-client.ts +488 -0
- package/src/types.ts +263 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndraClient - CLI wrapper for indra_db
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Spawning indra CLI subprocess
|
|
6
|
+
* - Auto-installation if binary not found
|
|
7
|
+
* - Database initialization
|
|
8
|
+
* - JSON parsing and error handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { mkdir } from "fs/promises";
|
|
16
|
+
import type {
|
|
17
|
+
Thought,
|
|
18
|
+
Edge,
|
|
19
|
+
SearchResult,
|
|
20
|
+
Neighbor,
|
|
21
|
+
Commit,
|
|
22
|
+
Branch,
|
|
23
|
+
DatabaseStatus,
|
|
24
|
+
ListThoughtsResponse,
|
|
25
|
+
SearchResponse,
|
|
26
|
+
NeighborsResponse,
|
|
27
|
+
LogResponse,
|
|
28
|
+
BranchesResponse,
|
|
29
|
+
CommitResponse,
|
|
30
|
+
Diff,
|
|
31
|
+
TraversalDirection,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
import { IndraError } from "./types.js";
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Configuration
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export interface IndraClientConfig {
|
|
40
|
+
/** Path to the database file. Defaults to ./thoughts.indra or ~/.thoughts.indra */
|
|
41
|
+
databasePath?: string;
|
|
42
|
+
/** Path to the indra binary. Defaults to searching PATH, then bundled, then auto-install */
|
|
43
|
+
binaryPath?: string;
|
|
44
|
+
/** Whether to auto-commit after each mutation. Defaults to true */
|
|
45
|
+
autoCommit?: boolean;
|
|
46
|
+
/** Timeout for CLI commands in milliseconds. Defaults to 30000 */
|
|
47
|
+
timeout?: number;
|
|
48
|
+
/** Whether to auto-initialize the database if it doesn't exist. Defaults to true */
|
|
49
|
+
autoInit?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_CONFIG: Required<IndraClientConfig> = {
|
|
53
|
+
databasePath: "", // Will be resolved dynamically
|
|
54
|
+
binaryPath: "", // Will be resolved dynamically
|
|
55
|
+
autoCommit: true,
|
|
56
|
+
timeout: 30000,
|
|
57
|
+
autoInit: true,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Binary Management
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Attempts to find or install the indra binary.
|
|
66
|
+
* Priority:
|
|
67
|
+
* 1. Explicit path in config
|
|
68
|
+
* 2. System PATH
|
|
69
|
+
* 3. Bundled binary in ./bin
|
|
70
|
+
* 4. Auto-install via cargo
|
|
71
|
+
*/
|
|
72
|
+
async function resolveBinaryPath(configPath?: string): Promise<string> {
|
|
73
|
+
// 1. Explicit config path
|
|
74
|
+
if (configPath && existsSync(configPath)) {
|
|
75
|
+
return configPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Check system PATH
|
|
79
|
+
const pathBinary = await findInPath("indra");
|
|
80
|
+
if (pathBinary) {
|
|
81
|
+
return pathBinary;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Check bundled binary
|
|
85
|
+
const bundledPath = join(dirname(import.meta.dir), "bin", "indra");
|
|
86
|
+
if (existsSync(bundledPath)) {
|
|
87
|
+
return bundledPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. Auto-install via cargo
|
|
91
|
+
console.error("[indra_db_mcp] indra binary not found. Installing via cargo...");
|
|
92
|
+
await installViaCargo();
|
|
93
|
+
|
|
94
|
+
// Check PATH again after install
|
|
95
|
+
const installed = await findInPath("indra");
|
|
96
|
+
if (installed) {
|
|
97
|
+
return installed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Failed to find or install indra binary. Please install manually:\n" +
|
|
102
|
+
" cargo install indra_db --features hf-embeddings\n" +
|
|
103
|
+
"Or download from: https://github.com/moonstripe/indra_db/releases"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function findInPath(binary: string): Promise<string | null> {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const proc = spawn("which", [binary]);
|
|
110
|
+
let stdout = "";
|
|
111
|
+
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
112
|
+
proc.on("close", (code) => {
|
|
113
|
+
if (code === 0 && stdout.trim()) {
|
|
114
|
+
resolve(stdout.trim());
|
|
115
|
+
} else {
|
|
116
|
+
resolve(null);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
proc.on("error", () => resolve(null));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function installViaCargo(): Promise<void> {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
console.error("[indra_db_mcp] Running: cargo install indra_db --features hf-embeddings");
|
|
126
|
+
|
|
127
|
+
const proc = spawn("cargo", ["install", "indra_db", "--features", "hf-embeddings"], {
|
|
128
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
proc.on("close", (code) => {
|
|
132
|
+
if (code === 0) {
|
|
133
|
+
console.error("[indra_db_mcp] Successfully installed indra_db");
|
|
134
|
+
resolve();
|
|
135
|
+
} else {
|
|
136
|
+
reject(new Error(`cargo install failed with exit code ${code}`));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
proc.on("error", (err) => {
|
|
141
|
+
reject(new Error(`Failed to run cargo: ${err.message}. Is Rust installed?`));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Database Path Resolution
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
function resolveDatabasePath(): string {
|
|
151
|
+
// 1. Check environment variable
|
|
152
|
+
const envPath = process.env.INDRA_DB_PATH;
|
|
153
|
+
if (envPath) {
|
|
154
|
+
// If explicitly set, use global path
|
|
155
|
+
if (envPath.startsWith("~")) {
|
|
156
|
+
return join(homedir(), envPath.slice(1));
|
|
157
|
+
}
|
|
158
|
+
return envPath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. Default to local directory for development
|
|
162
|
+
return join(process.cwd(), "thoughts.indra");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// IndraClient
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
export class IndraClient {
|
|
170
|
+
private config: Required<IndraClientConfig>;
|
|
171
|
+
private binaryPath: string | null = null;
|
|
172
|
+
private initialized = false;
|
|
173
|
+
|
|
174
|
+
constructor(config: IndraClientConfig = {}) {
|
|
175
|
+
this.config = {
|
|
176
|
+
...DEFAULT_CONFIG,
|
|
177
|
+
...config,
|
|
178
|
+
databasePath: config.databasePath || resolveDatabasePath(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --------------------------------------------------------------------------
|
|
183
|
+
// Initialization
|
|
184
|
+
// --------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
private async ensureReady(): Promise<void> {
|
|
187
|
+
if (!this.binaryPath) {
|
|
188
|
+
this.binaryPath = await resolveBinaryPath(this.config.binaryPath);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!this.initialized && this.config.autoInit) {
|
|
192
|
+
await this.initializeIfNeeded();
|
|
193
|
+
this.initialized = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async initializeIfNeeded(): Promise<void> {
|
|
198
|
+
if (!existsSync(this.config.databasePath)) {
|
|
199
|
+
// Ensure parent directory exists
|
|
200
|
+
const parentDir = dirname(this.config.databasePath);
|
|
201
|
+
if (!existsSync(parentDir)) {
|
|
202
|
+
await mkdir(parentDir, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.error(`[indra_db_mcp] Initializing new database at ${this.config.databasePath}`);
|
|
206
|
+
// Pass skipInit=true to prevent infinite recursion
|
|
207
|
+
await this.exec(["init"], true);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --------------------------------------------------------------------------
|
|
212
|
+
// CLI Execution
|
|
213
|
+
// --------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
private async exec<T = unknown>(args: string[], skipInit = false): Promise<T> {
|
|
216
|
+
// Resolve binary if not already done
|
|
217
|
+
if (!this.binaryPath) {
|
|
218
|
+
this.binaryPath = await resolveBinaryPath(this.config.binaryPath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Auto-initialize database if needed (but not for init command itself)
|
|
222
|
+
if (!skipInit && !this.initialized && this.config.autoInit) {
|
|
223
|
+
await this.initializeIfNeeded();
|
|
224
|
+
this.initialized = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fullArgs = ["-d", this.config.databasePath, "-f", "json", ...args];
|
|
228
|
+
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const proc = spawn(this.binaryPath!, fullArgs, {
|
|
231
|
+
timeout: this.config.timeout,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let stdout = "";
|
|
235
|
+
let stderr = "";
|
|
236
|
+
|
|
237
|
+
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
238
|
+
proc.stderr.on("data", (d) => (stderr += d.toString()));
|
|
239
|
+
|
|
240
|
+
proc.on("close", (code) => {
|
|
241
|
+
if (code === 0) {
|
|
242
|
+
try {
|
|
243
|
+
// Handle empty output (some commands may not return JSON)
|
|
244
|
+
if (!stdout.trim()) {
|
|
245
|
+
resolve({} as T);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
resolve(JSON.parse(stdout) as T);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// If JSON parsing fails, return raw output
|
|
251
|
+
resolve(stdout as unknown as T);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
reject(
|
|
255
|
+
new IndraError(
|
|
256
|
+
`Command failed: indra ${args.join(" ")}`,
|
|
257
|
+
code ?? 1,
|
|
258
|
+
stderr || stdout,
|
|
259
|
+
["indra", ...fullArgs]
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
proc.on("error", (err) => {
|
|
266
|
+
reject(
|
|
267
|
+
new IndraError(
|
|
268
|
+
`Failed to execute indra: ${err.message}`,
|
|
269
|
+
-1,
|
|
270
|
+
err.message,
|
|
271
|
+
["indra", ...fullArgs]
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --------------------------------------------------------------------------
|
|
279
|
+
// Thought Operations
|
|
280
|
+
// --------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a new thought in the knowledge graph.
|
|
284
|
+
* The thought will be assigned embeddings automatically for semantic search.
|
|
285
|
+
*/
|
|
286
|
+
async createThought(content: string, options?: { id?: string }): Promise<Thought> {
|
|
287
|
+
const args = ["create", content];
|
|
288
|
+
if (options?.id) {
|
|
289
|
+
args.push("--id", options.id);
|
|
290
|
+
}
|
|
291
|
+
const result = await this.exec<Thought>(args);
|
|
292
|
+
|
|
293
|
+
if (this.config.autoCommit) {
|
|
294
|
+
await this.commit(`Create thought: ${options?.id || result.id}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Retrieve a thought by its ID.
|
|
302
|
+
*/
|
|
303
|
+
async getThought(id: string): Promise<Thought> {
|
|
304
|
+
return this.exec<Thought>(["get", id]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Update the content of an existing thought.
|
|
309
|
+
* This creates a new version while preserving history.
|
|
310
|
+
*/
|
|
311
|
+
async updateThought(id: string, content: string): Promise<Thought> {
|
|
312
|
+
const result = await this.exec<Thought>(["update", id, content]);
|
|
313
|
+
|
|
314
|
+
if (this.config.autoCommit) {
|
|
315
|
+
await this.commit(`Update thought: ${id}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Delete a thought from the current state.
|
|
323
|
+
* The thought remains in history and can be recovered via branching.
|
|
324
|
+
*/
|
|
325
|
+
async deleteThought(id: string): Promise<void> {
|
|
326
|
+
await this.exec(["delete", id]);
|
|
327
|
+
|
|
328
|
+
if (this.config.autoCommit) {
|
|
329
|
+
await this.commit(`Delete thought: ${id}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* List all thoughts in the current state.
|
|
335
|
+
*/
|
|
336
|
+
async listThoughts(): Promise<ListThoughtsResponse> {
|
|
337
|
+
return this.exec<ListThoughtsResponse>(["list"]);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --------------------------------------------------------------------------
|
|
341
|
+
// Relationship Operations
|
|
342
|
+
// --------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create a typed relationship between two thoughts.
|
|
346
|
+
*/
|
|
347
|
+
async relate(
|
|
348
|
+
sourceId: string,
|
|
349
|
+
targetId: string,
|
|
350
|
+
edgeType: string = "relates_to",
|
|
351
|
+
options?: { weight?: number }
|
|
352
|
+
): Promise<Edge> {
|
|
353
|
+
const args = ["relate", sourceId, targetId, "-t", edgeType];
|
|
354
|
+
if (options?.weight !== undefined) {
|
|
355
|
+
args.push("-w", options.weight.toString());
|
|
356
|
+
}
|
|
357
|
+
const result = await this.exec<Edge>(args);
|
|
358
|
+
|
|
359
|
+
if (this.config.autoCommit) {
|
|
360
|
+
await this.commit(`Relate ${sourceId} --[${edgeType}]--> ${targetId}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Remove a relationship between two thoughts.
|
|
368
|
+
*/
|
|
369
|
+
async unrelate(sourceId: string, targetId: string, edgeType?: string): Promise<void> {
|
|
370
|
+
const args = ["unrelate", sourceId, targetId];
|
|
371
|
+
if (edgeType) {
|
|
372
|
+
args.push("-t", edgeType);
|
|
373
|
+
}
|
|
374
|
+
await this.exec(args);
|
|
375
|
+
|
|
376
|
+
if (this.config.autoCommit) {
|
|
377
|
+
await this.commit(`Unrelate ${sourceId} from ${targetId}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get all neighbors of a thought (connected via edges).
|
|
383
|
+
*/
|
|
384
|
+
async getNeighbors(
|
|
385
|
+
id: string,
|
|
386
|
+
direction: TraversalDirection = "both"
|
|
387
|
+
): Promise<NeighborsResponse> {
|
|
388
|
+
const args = ["neighbors", id];
|
|
389
|
+
if (direction !== "both") {
|
|
390
|
+
args.push("-d", direction);
|
|
391
|
+
}
|
|
392
|
+
return this.exec<NeighborsResponse>(args);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --------------------------------------------------------------------------
|
|
396
|
+
// Search Operations
|
|
397
|
+
// --------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Search thoughts by semantic similarity.
|
|
401
|
+
* Uses vector embeddings to find conceptually related thoughts.
|
|
402
|
+
*/
|
|
403
|
+
async search(query: string, limit: number = 10): Promise<SearchResponse> {
|
|
404
|
+
return this.exec<SearchResponse>(["search", query, "-l", limit.toString()]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --------------------------------------------------------------------------
|
|
408
|
+
// Versioning Operations
|
|
409
|
+
// --------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Commit current changes with a message.
|
|
413
|
+
* Creates a new snapshot in the history DAG.
|
|
414
|
+
*/
|
|
415
|
+
async commit(message: string): Promise<CommitResponse> {
|
|
416
|
+
return this.exec<CommitResponse>(["commit", message]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get commit history for the current branch.
|
|
421
|
+
*/
|
|
422
|
+
async log(limit?: number): Promise<LogResponse> {
|
|
423
|
+
const args = ["log"];
|
|
424
|
+
if (limit) {
|
|
425
|
+
args.push("-l", limit.toString());
|
|
426
|
+
}
|
|
427
|
+
return this.exec<LogResponse>(args);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Create a new branch from the current HEAD.
|
|
432
|
+
*/
|
|
433
|
+
async createBranch(name: string): Promise<Branch> {
|
|
434
|
+
return this.exec<Branch>(["branch", name]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Switch to a different branch.
|
|
439
|
+
*/
|
|
440
|
+
async checkout(branchName: string): Promise<void> {
|
|
441
|
+
await this.exec(["checkout", branchName]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* List all branches.
|
|
446
|
+
*/
|
|
447
|
+
async listBranches(): Promise<BranchesResponse> {
|
|
448
|
+
return this.exec<BranchesResponse>(["branches"]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Show diff between two commits.
|
|
453
|
+
*/
|
|
454
|
+
async diff(from?: string, to?: string): Promise<Diff> {
|
|
455
|
+
const args = ["diff"];
|
|
456
|
+
if (from) args.push(from);
|
|
457
|
+
if (to) args.push(to);
|
|
458
|
+
return this.exec<Diff>(args);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get current database status.
|
|
463
|
+
*/
|
|
464
|
+
async status(): Promise<DatabaseStatus> {
|
|
465
|
+
return this.exec<DatabaseStatus>(["status"]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// --------------------------------------------------------------------------
|
|
469
|
+
// Utility
|
|
470
|
+
// --------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get the database path being used.
|
|
474
|
+
*/
|
|
475
|
+
getDatabasePath(): string {
|
|
476
|
+
return this.config.databasePath;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Manually trigger initialization.
|
|
481
|
+
*/
|
|
482
|
+
async init(): Promise<void> {
|
|
483
|
+
await this.ensureReady();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Export a default instance that can be reconfigured
|
|
488
|
+
export const defaultClient = new IndraClient();
|