keystone-cli 2.0.1 → 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 +30 -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/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 +46 -17
- 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 +174 -85
- 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
|
|
|
@@ -427,6 +428,9 @@ expression:
|
|
|
427
428
|
strict: true
|
|
428
429
|
```
|
|
429
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
|
+
|
|
430
434
|
---
|
|
431
435
|
|
|
432
436
|
## <a id="step-types">🏗️ Step Types</a>
|
|
@@ -604,6 +608,9 @@ All steps support common features:
|
|
|
604
608
|
- `outputRetries`: Max retries for output validation failures.
|
|
605
609
|
- `repairStrategy`: Strategy for output repair (`reask`, `repair`, `hybrid`).
|
|
606
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
|
+
|
|
607
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).
|
|
608
615
|
|
|
609
616
|
### Engine Steps
|
|
@@ -678,6 +685,8 @@ Allow the LLM to switch to a specialist agent mid-step by defining `allowedHando
|
|
|
678
685
|
allowedHandoffs: [handoff-specialist]
|
|
679
686
|
```
|
|
680
687
|
|
|
688
|
+
To prevent infinite loops, handoffs are limited to **20** occurrences per step by default.
|
|
689
|
+
|
|
681
690
|
Agent prompts can use `${{ }}` expressions (evaluated against the workflow context) for dynamic system prompts.
|
|
682
691
|
|
|
683
692
|
```markdown
|
|
@@ -1223,18 +1232,26 @@ Input keys passed via `-i key=val` must be alphanumeric/underscore and cannot be
|
|
|
1223
1232
|
## <a id="security">🛡️ Security</a>
|
|
1224
1233
|
|
|
1225
1234
|
### Shell Execution
|
|
1226
|
-
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.
|
|
1227
1240
|
|
|
1241
|
+
To run complex commands or bypass allowlist checks, set `allowInsecure: true` on the step. Prefer `${{ escape(...) }}` when interpolating user input.
|
|
1242
|
+
|
|
1243
|
+
```yaml
|
|
1228
1244
|
- id: deploy
|
|
1229
1245
|
type: shell
|
|
1230
1246
|
run: ./deploy.sh ${{ inputs.env }}
|
|
1247
|
+
# Required if inputs.env might contain special characters or for complex scripts
|
|
1231
1248
|
allowInsecure: true
|
|
1232
1249
|
```
|
|
1233
1250
|
|
|
1234
1251
|
#### Troubleshooting Security Errors
|
|
1235
|
-
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.
|
|
1236
1253
|
- **Fix 1**: Use `${{ escape(steps.id.output) }}` for any dynamic values.
|
|
1237
|
-
- **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.
|
|
1238
1255
|
|
|
1239
1256
|
### Expression Safety
|
|
1240
1257
|
Expressions `${{ }}` are evaluated using a safe AST parser (`jsep`) which:
|
|
@@ -1288,8 +1305,14 @@ graph TD
|
|
|
1288
1305
|
EX --> Wait[Wait Step]
|
|
1289
1306
|
EX --> Join[Join Step]
|
|
1290
1307
|
EX --> Blueprint[Blueprint Step]
|
|
1308
|
+
EX --> Dynamic[Dynamic Executor]
|
|
1309
|
+
EX --> Plan[Plan Executor]
|
|
1291
1310
|
|
|
1292
|
-
|
|
1311
|
+
subgraph "LLM Subsystem"
|
|
1312
|
+
LLM --> ToolManager[Tool Manager]
|
|
1313
|
+
LLM --> StreamHandler[Stream Handler]
|
|
1314
|
+
ToolManager --> Adapter[LLM Adapter (AI SDK)]
|
|
1315
|
+
end
|
|
1293
1316
|
Adapter --> Providers[OpenAI, Anthropic, Gemini, Copilot, etc.]
|
|
1294
1317
|
LLM --> MCPClient[MCP Client]
|
|
1295
1318
|
```
|
|
@@ -1297,10 +1320,13 @@ graph TD
|
|
|
1297
1320
|
## <a id="project-structure">📂 Project Structure</a>
|
|
1298
1321
|
|
|
1299
1322
|
- `src/cli.ts`: CLI entry point.
|
|
1323
|
+
- `src/commands/`: Command implementations (run, ui, config, etc.).
|
|
1300
1324
|
- `src/db/`: SQLite persistence layer.
|
|
1301
1325
|
- `src/runner/`: The core execution engine, handles parallelization and retries.
|
|
1302
1326
|
- `src/parser/`: Zod-powered validation for workflows and agents.
|
|
1303
1327
|
- `src/expression/`: `${{ }}` expression evaluator.
|
|
1328
|
+
- `src/providers/`: Custom AI provider implementations.
|
|
1329
|
+
- `src/scripts/`: Build and utility scripts.
|
|
1304
1330
|
- `src/templates/`: Bundled workflow and agent templates.
|
|
1305
1331
|
- `src/ui/`: Ink-powered TUI dashboard.
|
|
1306
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.0
|
|
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
|
}
|