keystone-cli 2.0.0 → 2.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 +43 -4
- package/package.json +4 -1
- package/src/cli.ts +1 -0
- package/src/commands/event.ts +9 -0
- package/src/commands/run.ts +17 -0
- package/src/db/dynamic-state-manager.ts +12 -9
- package/src/db/memory-db.test.ts +19 -1
- package/src/db/memory-db.ts +101 -22
- package/src/db/workflow-db.ts +181 -9
- package/src/expression/evaluator.ts +4 -1
- package/src/parser/config-schema.ts +6 -0
- package/src/parser/schema.ts +1 -0
- package/src/runner/__test__/llm-test-setup.ts +43 -11
- package/src/runner/durable-timers.test.ts +1 -1
- package/src/runner/executors/dynamic-executor.ts +125 -88
- package/src/runner/executors/engine-executor.ts +10 -39
- package/src/runner/executors/file-executor.ts +67 -0
- package/src/runner/executors/foreach-executor.ts +170 -17
- package/src/runner/executors/human-executor.ts +18 -0
- package/src/runner/executors/llm/stream-handler.ts +103 -0
- package/src/runner/executors/llm/tool-manager.ts +360 -0
- package/src/runner/executors/llm-executor.ts +288 -555
- package/src/runner/executors/memory-executor.ts +41 -34
- package/src/runner/executors/shell-executor.ts +96 -52
- package/src/runner/executors/subworkflow-executor.ts +16 -0
- package/src/runner/executors/types.ts +3 -1
- package/src/runner/executors/verification_fixes.test.ts +46 -0
- package/src/runner/join-scheduling.test.ts +2 -1
- package/src/runner/llm-adapter.integration.test.ts +10 -5
- package/src/runner/llm-adapter.ts +57 -18
- package/src/runner/llm-clarification.test.ts +4 -1
- package/src/runner/llm-executor.test.ts +21 -7
- package/src/runner/mcp-client.ts +36 -2
- package/src/runner/mcp-server.ts +65 -36
- package/src/runner/recovery-security.test.ts +5 -2
- package/src/runner/reflexion.test.ts +6 -3
- package/src/runner/services/context-builder.ts +13 -4
- package/src/runner/services/workflow-validator.ts +2 -1
- package/src/runner/standard-tools-ast.test.ts +4 -2
- package/src/runner/standard-tools-execution.test.ts +14 -1
- package/src/runner/standard-tools-integration.test.ts +6 -0
- package/src/runner/standard-tools.ts +13 -10
- package/src/runner/step-executor.ts +2 -2
- package/src/runner/tool-integration.test.ts +4 -1
- package/src/runner/workflow-runner.test.ts +23 -12
- package/src/runner/workflow-runner.ts +172 -79
- package/src/runner/workflow-state.ts +181 -111
- package/src/ui/dashboard.tsx +17 -3
- package/src/utils/config-loader.ts +4 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/context-injector.test.ts +27 -27
- package/src/utils/context-injector.ts +68 -26
- package/src/utils/process-sandbox.ts +138 -148
- package/src/utils/redactor.ts +39 -9
- package/src/utils/resource-loader.ts +24 -19
- package/src/utils/sandbox.ts +6 -0
- package/src/utils/stream-utils.ts +58 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://bun.sh)
|
|
8
8
|
[](https://www.npmjs.com/package/keystone-cli)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://deepwiki.com/mhingston/keystone-cli)
|
|
10
11
|
|
|
11
12
|
A local-first, declarative, agentic workflow orchestrator built on **Bun**.
|
|
12
13
|
|
|
@@ -237,6 +238,10 @@ storage:
|
|
|
237
238
|
|
|
238
239
|
expression:
|
|
239
240
|
strict: false
|
|
241
|
+
|
|
242
|
+
logging:
|
|
243
|
+
suppress_security_warning: false
|
|
244
|
+
suppress_ai_sdk_warnings: false
|
|
240
245
|
```
|
|
241
246
|
|
|
242
247
|
### Storage Configuration
|
|
@@ -246,6 +251,13 @@ The `storage` section controls data retention and security for workflow runs:
|
|
|
246
251
|
- **`retention_days`**: Sets the default window used by `keystone maintenance` / `keystone prune` commands to clean up old run data.
|
|
247
252
|
- **`redact_secrets_at_rest`**: Controls whether secret inputs and known secrets are redacted before storing run data (default `true`).
|
|
248
253
|
|
|
254
|
+
### Logging Configuration
|
|
255
|
+
|
|
256
|
+
The `logging` section allows you to suppress warnings:
|
|
257
|
+
|
|
258
|
+
- **`suppress_security_warning`**: Silences the "Security Warning" about running workflows from untrusted sources (default `false`).
|
|
259
|
+
- **`suppress_ai_sdk_warnings`**: Silences internal warnings from the Vercel AI SDK, such as compatibility mode messages (default `false`).
|
|
260
|
+
|
|
249
261
|
### Bring Your Own Provider (BYOP)
|
|
250
262
|
|
|
251
263
|
Keystone uses the **Vercel AI SDK**, allowing you to use any compatible provider. You must install the provider package (e.g., `@ai-sdk/openai`, `ai-sdk-provider-gemini-cli`) so Keystone can resolve it.
|
|
@@ -416,6 +428,9 @@ expression:
|
|
|
416
428
|
strict: true
|
|
417
429
|
```
|
|
418
430
|
|
|
431
|
+
> [!NOTE]
|
|
432
|
+
> When `strict: false` (default), evaluation errors in outputs will be reported as warnings and the value will be set to `null` to allow the workflow to potentially continue.
|
|
433
|
+
|
|
419
434
|
---
|
|
420
435
|
|
|
421
436
|
## <a id="step-types">🏗️ Step Types</a>
|
|
@@ -593,6 +608,9 @@ All steps support common features:
|
|
|
593
608
|
- `outputRetries`: Max retries for output validation failures.
|
|
594
609
|
- `repairStrategy`: Strategy for output repair (`reask`, `repair`, `hybrid`).
|
|
595
610
|
|
|
611
|
+
> [!TIP]
|
|
612
|
+
> **Performance Optimization**: For `foreach` steps with very large datasets, Keystone may automatically skip output aggregation to prevent memory issues. Use file-based storage or external databases if you need to process tens of thousands of items.
|
|
613
|
+
|
|
596
614
|
Workflows also support a top-level `concurrency` field to limit how many steps can run in parallel across the entire workflow. This must resolve to a positive integer (number or expression).
|
|
597
615
|
|
|
598
616
|
### Engine Steps
|
|
@@ -667,6 +685,8 @@ Allow the LLM to switch to a specialist agent mid-step by defining `allowedHando
|
|
|
667
685
|
allowedHandoffs: [handoff-specialist]
|
|
668
686
|
```
|
|
669
687
|
|
|
688
|
+
To prevent infinite loops, handoffs are limited to **20** occurrences per step by default.
|
|
689
|
+
|
|
670
690
|
Agent prompts can use `${{ }}` expressions (evaluated against the workflow context) for dynamic system prompts.
|
|
671
691
|
|
|
672
692
|
```markdown
|
|
@@ -1212,18 +1232,26 @@ Input keys passed via `-i key=val` must be alphanumeric/underscore and cannot be
|
|
|
1212
1232
|
## <a id="security">🛡️ Security</a>
|
|
1213
1233
|
|
|
1214
1234
|
### Shell Execution
|
|
1215
|
-
Keystone
|
|
1235
|
+
Keystone strictly enforces an allowlist of characters (`alphanumeric`, `whitespace`, and `_./:@,+=~-`) to prevent shell injection.
|
|
1236
|
+
|
|
1237
|
+
- **Directory Traversal**: Commands containing `..` in a path are blocked by default for security.
|
|
1238
|
+
- **Denylist**: Commands like `rm`, `mkfs`, or `alias` are blocked via a configurable denylist in `config.yaml`, even if `allowInsecure: true` is set.
|
|
1239
|
+
- **Windows Support**: Keystone uses `cmd.exe /d /s /c` on Windows and `sh -c` on other platforms for consistent behavior.
|
|
1240
|
+
|
|
1241
|
+
To run complex commands or bypass allowlist checks, set `allowInsecure: true` on the step. Prefer `${{ escape(...) }}` when interpolating user input.
|
|
1216
1242
|
|
|
1243
|
+
```yaml
|
|
1217
1244
|
- id: deploy
|
|
1218
1245
|
type: shell
|
|
1219
1246
|
run: ./deploy.sh ${{ inputs.env }}
|
|
1247
|
+
# Required if inputs.env might contain special characters or for complex scripts
|
|
1220
1248
|
allowInsecure: true
|
|
1221
1249
|
```
|
|
1222
1250
|
|
|
1223
1251
|
#### Troubleshooting Security Errors
|
|
1224
|
-
If you see a `Security Error: Evaluated command contains shell metacharacters`, it means your command contains characters like `\n`, `|`, or
|
|
1252
|
+
If you see a `Security Error: Evaluated command contains shell metacharacters`, it means your command contains characters like `\n`, `|`, `&`, or quotes that are not in the strict allowlist.
|
|
1225
1253
|
- **Fix 1**: Use `${{ escape(steps.id.output) }}` for any dynamic values.
|
|
1226
|
-
- **Fix 2**: Set `allowInsecure: true` if the command naturally uses special characters
|
|
1254
|
+
- **Fix 2**: Set `allowInsecure: true` if the command naturally uses special characters.
|
|
1227
1255
|
|
|
1228
1256
|
### Expression Safety
|
|
1229
1257
|
Expressions `${{ }}` are evaluated using a safe AST parser (`jsep`) which:
|
|
@@ -1246,6 +1274,8 @@ Request steps enforce SSRF protections and require HTTPS by default. Cross-origi
|
|
|
1246
1274
|
graph TD
|
|
1247
1275
|
CLI[CLI Entry Point] --> WR[WorkflowRunner]
|
|
1248
1276
|
CLI --> MCPServer[MCP Server]
|
|
1277
|
+
Config[ConfigLoader] --> WR
|
|
1278
|
+
Config --> Adapter
|
|
1249
1279
|
|
|
1250
1280
|
subgraph "Core Orchestration"
|
|
1251
1281
|
WR --> Scheduler[WorkflowScheduler]
|
|
@@ -1275,8 +1305,14 @@ graph TD
|
|
|
1275
1305
|
EX --> Wait[Wait Step]
|
|
1276
1306
|
EX --> Join[Join Step]
|
|
1277
1307
|
EX --> Blueprint[Blueprint Step]
|
|
1308
|
+
EX --> Dynamic[Dynamic Executor]
|
|
1309
|
+
EX --> Plan[Plan Executor]
|
|
1278
1310
|
|
|
1279
|
-
|
|
1311
|
+
subgraph "LLM Subsystem"
|
|
1312
|
+
LLM --> ToolManager[Tool Manager]
|
|
1313
|
+
LLM --> StreamHandler[Stream Handler]
|
|
1314
|
+
ToolManager --> Adapter[LLM Adapter (AI SDK)]
|
|
1315
|
+
end
|
|
1280
1316
|
Adapter --> Providers[OpenAI, Anthropic, Gemini, Copilot, etc.]
|
|
1281
1317
|
LLM --> MCPClient[MCP Client]
|
|
1282
1318
|
```
|
|
@@ -1284,10 +1320,13 @@ graph TD
|
|
|
1284
1320
|
## <a id="project-structure">📂 Project Structure</a>
|
|
1285
1321
|
|
|
1286
1322
|
- `src/cli.ts`: CLI entry point.
|
|
1323
|
+
- `src/commands/`: Command implementations (run, ui, config, etc.).
|
|
1287
1324
|
- `src/db/`: SQLite persistence layer.
|
|
1288
1325
|
- `src/runner/`: The core execution engine, handles parallelization and retries.
|
|
1289
1326
|
- `src/parser/`: Zod-powered validation for workflows and agents.
|
|
1290
1327
|
- `src/expression/`: `${{ }}` expression evaluator.
|
|
1328
|
+
- `src/providers/`: Custom AI provider implementations.
|
|
1329
|
+
- `src/scripts/`: Build and utility scripts.
|
|
1291
1330
|
- `src/templates/`: Bundled workflow and agent templates.
|
|
1292
1331
|
- `src/ui/`: Ink-powered TUI dashboard.
|
|
1293
1332
|
- `src/utils/`: Shared utilities (auth, redaction, config loading).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keystone-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A local-first, declarative, agentic workflow orchestrator built on Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,8 +42,10 @@
|
|
|
42
42
|
"ink-spinner": "^5.0.0",
|
|
43
43
|
"js-yaml": "^4.1.0",
|
|
44
44
|
"jsep": "^1.4.0",
|
|
45
|
+
"minimatch": "^10.1.1",
|
|
45
46
|
"react": "^19.0.0",
|
|
46
47
|
"sqlite-vec": "0.1.6",
|
|
48
|
+
"yaml": "^2.8.2",
|
|
47
49
|
"zod": "^3.25.76",
|
|
48
50
|
"zod-to-json-schema": "^3.25.1"
|
|
49
51
|
},
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"@types/bun": "^1.3.5",
|
|
56
58
|
"@types/dagre": "^0.7.53",
|
|
57
59
|
"@types/js-yaml": "^4.0.9",
|
|
60
|
+
"@types/minimatch": "^6.0.0",
|
|
58
61
|
"@types/node": "^25.0.3",
|
|
59
62
|
"react-devtools-core": "^7.0.1"
|
|
60
63
|
},
|
package/src/cli.ts
CHANGED
package/src/commands/event.ts
CHANGED
|
@@ -25,5 +25,14 @@ export function registerEventCommand(program: Command): void {
|
|
|
25
25
|
}
|
|
26
26
|
await db.storeEvent(name, data);
|
|
27
27
|
console.log(`✓ Event '${name}' triggered.`);
|
|
28
|
+
|
|
29
|
+
// Check for workflows waiting for this event
|
|
30
|
+
const suspendedRunIds = await db.getSuspendedStepsForEvent(name);
|
|
31
|
+
if (suspendedRunIds.length > 0) {
|
|
32
|
+
console.log(`\nFound ${suspendedRunIds.length} workflow(s) waiting for this event:`);
|
|
33
|
+
for (const runId of suspendedRunIds) {
|
|
34
|
+
console.log(` - Run ${runId}: Resume with \`keystone resume ${runId}\``);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
});
|
|
29
38
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -24,6 +24,23 @@ export function registerRunCommand(program: Command): void {
|
|
|
24
24
|
.option('--resume', 'Resume the last run of this workflow if it failed or was paused')
|
|
25
25
|
.option('--explain', 'Show detailed error context with suggestions on failure')
|
|
26
26
|
.action(async (workflowPathArg, options) => {
|
|
27
|
+
// Security Warning
|
|
28
|
+
if (!options.events) {
|
|
29
|
+
console.warn(
|
|
30
|
+
'\x1b[33m%s\x1b[0m',
|
|
31
|
+
'⚠️ SECURITY WARNING: This tool executes code from the current directory.'
|
|
32
|
+
);
|
|
33
|
+
console.warn(
|
|
34
|
+
'\x1b[33m%s\x1b[0m',
|
|
35
|
+
' - Local provider scripts in ./providers/ are loaded and executed.'
|
|
36
|
+
);
|
|
37
|
+
console.warn(
|
|
38
|
+
'\x1b[33m%s\x1b[0m',
|
|
39
|
+
' - Ensure you trust the code in this directory before running.'
|
|
40
|
+
);
|
|
41
|
+
console.warn('');
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
const inputs = parseInputs(options.input);
|
|
28
45
|
let resolvedPath: string | undefined;
|
|
29
46
|
|
|
@@ -175,17 +175,20 @@ export class DynamicStateManager {
|
|
|
175
175
|
const db = this.getDatabase();
|
|
176
176
|
const now = new Date().toISOString();
|
|
177
177
|
|
|
178
|
-
//
|
|
179
|
-
const
|
|
180
|
-
.prepare(
|
|
181
|
-
.get(stateId) as { replan_count: number };
|
|
182
|
-
const replanCount = current?.replan_count || 0;
|
|
183
|
-
|
|
184
|
-
db.prepare(`
|
|
178
|
+
// Use atomic SQL update to increment replan_count and set new plan
|
|
179
|
+
const result = db
|
|
180
|
+
.prepare(`
|
|
185
181
|
UPDATE dynamic_workflow_state
|
|
186
|
-
SET generated_plan = ?, status = ?, updated_at = ?, replan_count =
|
|
182
|
+
SET generated_plan = ?, status = ?, updated_at = ?, replan_count = replan_count + 1
|
|
187
183
|
WHERE id = ?
|
|
188
|
-
|
|
184
|
+
RETURNING replan_count
|
|
185
|
+
`)
|
|
186
|
+
.get(JSON.stringify(plan), status, now, stateId) as { replan_count: number } | undefined;
|
|
187
|
+
|
|
188
|
+
if (!result) {
|
|
189
|
+
throw new Error(`Failed to update dynamic state: ${stateId}`);
|
|
190
|
+
}
|
|
191
|
+
const replanCount = result.replan_count;
|
|
189
192
|
|
|
190
193
|
// Delete previous execution records IF this is a re-plan (optional, but cleaner)
|
|
191
194
|
if (replanCount > 0) {
|
package/src/db/memory-db.test.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import { MemoryDb } from './memory-db';
|
|
4
|
+
import { setupSqlite } from './sqlite-setup';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
// Initialize SQLite with custom library for extensions
|
|
9
|
+
setupSqlite();
|
|
10
|
+
|
|
11
|
+
const TEST_DB = `.keystone/test-memory-${randomUUID()}.db`;
|
|
6
12
|
|
|
7
13
|
describe('MemoryDb', () => {
|
|
8
14
|
// Clean up previous runs
|
|
@@ -11,6 +17,7 @@ describe('MemoryDb', () => {
|
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
const db = new MemoryDb(TEST_DB);
|
|
20
|
+
console.log(`[MemoryDb Test] DB: ${TEST_DB}, Vector Ready: ${db.isVectorReady}`);
|
|
14
21
|
|
|
15
22
|
afterAll(() => {
|
|
16
23
|
db.close();
|
|
@@ -20,6 +27,7 @@ describe('MemoryDb', () => {
|
|
|
20
27
|
});
|
|
21
28
|
|
|
22
29
|
test('should initialize and store embedding', async () => {
|
|
30
|
+
if (!db.isVectorReady) return;
|
|
23
31
|
const id = await db.store('hello world', Array(384).fill(0.1), { tag: 'test' });
|
|
24
32
|
expect(id).toBeDefined();
|
|
25
33
|
expect(typeof id).toBe('string');
|
|
@@ -32,6 +40,8 @@ describe('MemoryDb', () => {
|
|
|
32
40
|
|
|
33
41
|
const db1536 = new MemoryDb(testDb1536, DIM_1536);
|
|
34
42
|
try {
|
|
43
|
+
if (!db1536.isVectorReady) return;
|
|
44
|
+
|
|
35
45
|
const id = await db1536.store('hi', Array(DIM_1536).fill(0.5));
|
|
36
46
|
expect(id).toBeDefined();
|
|
37
47
|
|
|
@@ -56,6 +66,12 @@ describe('MemoryDb', () => {
|
|
|
56
66
|
|
|
57
67
|
// Let's just test that we can use different dimensions on the same DB file.
|
|
58
68
|
const db1 = new MemoryDb(testDbMismatch, 128);
|
|
69
|
+
if (!db1.isVectorReady) {
|
|
70
|
+
db1.close();
|
|
71
|
+
if (fs.existsSync(testDbMismatch)) fs.unlinkSync(testDbMismatch);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
await db1.store('test128', Array(128).fill(0));
|
|
60
76
|
db1.close();
|
|
61
77
|
|
|
@@ -71,6 +87,7 @@ describe('MemoryDb', () => {
|
|
|
71
87
|
});
|
|
72
88
|
|
|
73
89
|
test('should search and retrieve result', async () => {
|
|
90
|
+
if (!db.isVectorReady) return;
|
|
74
91
|
// Store another item to search for
|
|
75
92
|
await db.store('search target', Array(384).fill(0.9), { tag: 'target' });
|
|
76
93
|
|
|
@@ -81,6 +98,7 @@ describe('MemoryDb', () => {
|
|
|
81
98
|
});
|
|
82
99
|
|
|
83
100
|
test('should fail gracefully with invalid dimensions', async () => {
|
|
101
|
+
if (!db.isVectorReady) return;
|
|
84
102
|
// sqlite-vec requires fixed dimensions (384 defined in schema)
|
|
85
103
|
// bun:sqlite usually throws an error for constraint violations
|
|
86
104
|
let error: unknown;
|
package/src/db/memory-db.ts
CHANGED
|
@@ -66,33 +66,83 @@ export class MemoryDb {
|
|
|
66
66
|
// Cache connections by path to avoid reloading extensions
|
|
67
67
|
private static connectionCache = new Map<string, { db: Database; refCount: number }>();
|
|
68
68
|
private tableName: string;
|
|
69
|
+
private vectorReady = false;
|
|
70
|
+
|
|
71
|
+
get isVectorReady(): boolean {
|
|
72
|
+
return this.vectorReady;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Acquire a MemoryDb instance. This handles reference counting automatically.
|
|
77
|
+
*/
|
|
78
|
+
static acquire(dbPath = '.keystone/memory.db', embeddingDimension = 384): MemoryDb {
|
|
79
|
+
const cached = MemoryDb.connectionCache.get(dbPath);
|
|
80
|
+
if (cached) {
|
|
81
|
+
cached.refCount++;
|
|
82
|
+
// We return a new instance but it shares the underlying DB connection
|
|
83
|
+
return new MemoryDb(dbPath, embeddingDimension, cached.db);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create new connection
|
|
87
|
+
const instance = new MemoryDb(dbPath, embeddingDimension);
|
|
88
|
+
MemoryDb.connectionCache.set(dbPath, { db: instance.db, refCount: 1 });
|
|
89
|
+
return instance;
|
|
90
|
+
}
|
|
69
91
|
|
|
70
92
|
constructor(
|
|
71
93
|
public readonly dbPath = '.keystone/memory.db',
|
|
72
|
-
private readonly embeddingDimension = 384
|
|
94
|
+
private readonly embeddingDimension = 384,
|
|
95
|
+
existingDb?: Database
|
|
73
96
|
) {
|
|
74
97
|
// Ensure SQLite is set up with custom library on macOS (idempotent)
|
|
75
98
|
setupSqlite();
|
|
76
99
|
|
|
77
100
|
this.tableName = `vec_memory_${embeddingDimension}`;
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
this.db = cached.db;
|
|
101
|
+
|
|
102
|
+
if (existingDb) {
|
|
103
|
+
this.db = existingDb;
|
|
82
104
|
} else {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
// Check cache again in case direct constructor usage overlaps with cache
|
|
106
|
+
const cached = MemoryDb.connectionCache.get(dbPath);
|
|
107
|
+
if (cached) {
|
|
108
|
+
// This path shouldn't typically be hit if users use acquire(), but for safety:
|
|
109
|
+
cached.refCount++;
|
|
110
|
+
this.db = cached.db;
|
|
111
|
+
} else {
|
|
112
|
+
const dir = dirname(dbPath);
|
|
113
|
+
if (!existsSync(dir)) {
|
|
114
|
+
mkdirSync(dir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
this.db = new Database(dbPath, { create: true });
|
|
117
|
+
|
|
118
|
+
// Load sqlite-vec extension
|
|
119
|
+
try {
|
|
120
|
+
const extensionPath = resolveSqliteVecPath();
|
|
121
|
+
this.db.loadExtension(extensionPath);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// In some environments (e.g. standard Bun builds), dynamic extension loading might be disabled.
|
|
124
|
+
// We log a warning and proceed without vector support.
|
|
125
|
+
new ConsoleLogger().warn(
|
|
126
|
+
`⚠️ Vector DB: Failed to load sqlite-vec extension. Vector search will be unavailable. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
88
129
|
|
|
89
|
-
|
|
90
|
-
const extensionPath = resolveSqliteVecPath();
|
|
91
|
-
this.db.loadExtension(extensionPath);
|
|
130
|
+
this.initSchema();
|
|
92
131
|
|
|
93
|
-
|
|
132
|
+
// Seed cache
|
|
133
|
+
MemoryDb.connectionCache.set(dbPath, { db: this.db, refCount: 1 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
94
137
|
|
|
95
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Manually increment reference count.
|
|
140
|
+
* Useful when passing an instance to another component that should also own it.
|
|
141
|
+
*/
|
|
142
|
+
retain(): void {
|
|
143
|
+
const cached = MemoryDb.connectionCache.get(this.dbPath);
|
|
144
|
+
if (cached) {
|
|
145
|
+
cached.refCount++;
|
|
96
146
|
}
|
|
97
147
|
}
|
|
98
148
|
|
|
@@ -123,12 +173,29 @@ export class MemoryDb {
|
|
|
123
173
|
}
|
|
124
174
|
}
|
|
125
175
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
176
|
+
try {
|
|
177
|
+
this.db.run(`
|
|
178
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ${this.tableName} USING vec0(
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
embedding FLOAT[${this.embeddingDimension}]
|
|
181
|
+
);
|
|
182
|
+
`);
|
|
183
|
+
|
|
184
|
+
// Verify table actually exists (in case run() didn't throw but failed)
|
|
185
|
+
const tableExists = this.db
|
|
186
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='${this.tableName}'`)
|
|
187
|
+
.get();
|
|
188
|
+
this.vectorReady = !!tableExists;
|
|
189
|
+
|
|
190
|
+
if (!this.vectorReady) {
|
|
191
|
+
new ConsoleLogger().warn(`⚠️ Vector DB: Vector table '${this.tableName}' was not created.`);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.vectorReady = false;
|
|
195
|
+
new ConsoleLogger().warn(
|
|
196
|
+
`⚠️ Vector DB: Failed to create vector table. Vector search will be unavailable. Error: ${error}`
|
|
130
197
|
);
|
|
131
|
-
|
|
198
|
+
}
|
|
132
199
|
|
|
133
200
|
this.db.run(`
|
|
134
201
|
CREATE TABLE IF NOT EXISTS memory_metadata (
|
|
@@ -219,6 +286,14 @@ export class MemoryDb {
|
|
|
219
286
|
}));
|
|
220
287
|
}
|
|
221
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Release the connection. Decrements ref count and closes DB if 0.
|
|
291
|
+
* Alias for close() for backward compatibility.
|
|
292
|
+
*/
|
|
293
|
+
release(): void {
|
|
294
|
+
this.close();
|
|
295
|
+
}
|
|
296
|
+
|
|
222
297
|
close(): void {
|
|
223
298
|
const cached = MemoryDb.connectionCache.get(this.dbPath);
|
|
224
299
|
if (cached) {
|
|
@@ -228,8 +303,12 @@ export class MemoryDb {
|
|
|
228
303
|
MemoryDb.connectionCache.delete(this.dbPath);
|
|
229
304
|
}
|
|
230
305
|
} else {
|
|
231
|
-
// Fallback if not in cache for some reason
|
|
232
|
-
|
|
306
|
+
// Fallback if not in cache for some reason or already closed
|
|
307
|
+
try {
|
|
308
|
+
this.db.close();
|
|
309
|
+
} catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
233
312
|
}
|
|
234
313
|
}
|
|
235
314
|
}
|