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.
Files changed (57) hide show
  1. package/README.md +43 -4
  2. package/package.json +4 -1
  3. package/src/cli.ts +1 -0
  4. package/src/commands/event.ts +9 -0
  5. package/src/commands/run.ts +17 -0
  6. package/src/db/dynamic-state-manager.ts +12 -9
  7. package/src/db/memory-db.test.ts +19 -1
  8. package/src/db/memory-db.ts +101 -22
  9. package/src/db/workflow-db.ts +181 -9
  10. package/src/expression/evaluator.ts +4 -1
  11. package/src/parser/config-schema.ts +6 -0
  12. package/src/parser/schema.ts +1 -0
  13. package/src/runner/__test__/llm-test-setup.ts +43 -11
  14. package/src/runner/durable-timers.test.ts +1 -1
  15. package/src/runner/executors/dynamic-executor.ts +125 -88
  16. package/src/runner/executors/engine-executor.ts +10 -39
  17. package/src/runner/executors/file-executor.ts +67 -0
  18. package/src/runner/executors/foreach-executor.ts +170 -17
  19. package/src/runner/executors/human-executor.ts +18 -0
  20. package/src/runner/executors/llm/stream-handler.ts +103 -0
  21. package/src/runner/executors/llm/tool-manager.ts +360 -0
  22. package/src/runner/executors/llm-executor.ts +288 -555
  23. package/src/runner/executors/memory-executor.ts +41 -34
  24. package/src/runner/executors/shell-executor.ts +96 -52
  25. package/src/runner/executors/subworkflow-executor.ts +16 -0
  26. package/src/runner/executors/types.ts +3 -1
  27. package/src/runner/executors/verification_fixes.test.ts +46 -0
  28. package/src/runner/join-scheduling.test.ts +2 -1
  29. package/src/runner/llm-adapter.integration.test.ts +10 -5
  30. package/src/runner/llm-adapter.ts +57 -18
  31. package/src/runner/llm-clarification.test.ts +4 -1
  32. package/src/runner/llm-executor.test.ts +21 -7
  33. package/src/runner/mcp-client.ts +36 -2
  34. package/src/runner/mcp-server.ts +65 -36
  35. package/src/runner/recovery-security.test.ts +5 -2
  36. package/src/runner/reflexion.test.ts +6 -3
  37. package/src/runner/services/context-builder.ts +13 -4
  38. package/src/runner/services/workflow-validator.ts +2 -1
  39. package/src/runner/standard-tools-ast.test.ts +4 -2
  40. package/src/runner/standard-tools-execution.test.ts +14 -1
  41. package/src/runner/standard-tools-integration.test.ts +6 -0
  42. package/src/runner/standard-tools.ts +13 -10
  43. package/src/runner/step-executor.ts +2 -2
  44. package/src/runner/tool-integration.test.ts +4 -1
  45. package/src/runner/workflow-runner.test.ts +23 -12
  46. package/src/runner/workflow-runner.ts +172 -79
  47. package/src/runner/workflow-state.ts +181 -111
  48. package/src/ui/dashboard.tsx +17 -3
  49. package/src/utils/config-loader.ts +4 -0
  50. package/src/utils/constants.ts +4 -0
  51. package/src/utils/context-injector.test.ts +27 -27
  52. package/src/utils/context-injector.ts +68 -26
  53. package/src/utils/process-sandbox.ts +138 -148
  54. package/src/utils/redactor.ts +39 -9
  55. package/src/utils/resource-loader.ts +24 -19
  56. package/src/utils/sandbox.ts +6 -0
  57. package/src/utils/stream-utils.ts +58 -0
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  [![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=flat&logo=bun&logoColor=white)](https://bun.sh)
8
8
  [![npm version](https://img.shields.io/npm/v/keystone-cli.svg?style=flat)](https://www.npmjs.com/package/keystone-cli)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 blocks shell commands that match common injection/destructive patterns (like `rm -rf /` or pipes to shells). To run them, set `allowInsecure: true` on the step. Prefer `${{ escape(...) }}` when interpolating user input.
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 `&` that were not explicitly escaped or are not in the safe whitelist.
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 (like `echo "line1\nline2"`).
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
- LLM --> Adapter[LLM Adapter (AI SDK)]
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.0.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
@@ -57,6 +57,7 @@ registerGraphCommand(program);
57
57
  registerDocCommand(program);
58
58
  registerSchemaCommand(program);
59
59
  registerEventCommand(program);
60
+
60
61
  registerRunCommand(program);
61
62
 
62
63
  // Helper function used by remaining commands (rerun)
@@ -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
  }
@@ -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
- // Load current state to get replanCount
179
- const current = db
180
- .prepare('SELECT replan_count FROM dynamic_workflow_state WHERE id = ?')
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
- `).run(JSON.stringify(plan), status, now, replanCount, stateId);
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) {
@@ -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
- const TEST_DB = '.keystone/test-memory.db';
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;
@@ -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
- const cached = MemoryDb.connectionCache.get(dbPath);
79
- if (cached) {
80
- cached.refCount++;
81
- this.db = cached.db;
101
+
102
+ if (existingDb) {
103
+ this.db = existingDb;
82
104
  } else {
83
- const dir = dirname(dbPath);
84
- if (!existsSync(dir)) {
85
- mkdirSync(dir, { recursive: true });
86
- }
87
- this.db = new Database(dbPath, { create: true });
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
- // Load sqlite-vec extension
90
- const extensionPath = resolveSqliteVecPath();
91
- this.db.loadExtension(extensionPath);
130
+ this.initSchema();
92
131
 
93
- this.initSchema();
132
+ // Seed cache
133
+ MemoryDb.connectionCache.set(dbPath, { db: this.db, refCount: 1 });
134
+ }
135
+ }
136
+ }
94
137
 
95
- MemoryDb.connectionCache.set(dbPath, { db: this.db, refCount: 1 });
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
- this.db.run(`
127
- CREATE VIRTUAL TABLE IF NOT EXISTS ${this.tableName} USING vec0(
128
- id TEXT PRIMARY KEY,
129
- embedding FLOAT[${this.embeddingDimension}]
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
- this.db.close();
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
  }