twinclaw 1.3.2 → 1.4.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.
@@ -1,12 +1,10 @@
1
1
  import Database from 'better-sqlite3';
2
- import * as sqliteVec from 'sqlite-vec';
3
2
  import path from 'path';
4
3
  import fs from 'fs';
5
4
  import { getConfigValue } from '../config/json-config.js';
6
5
  import { getDatabasePath, ensureWorkspaceSubdirs } from '../config/workspace.js';
7
6
  ensureWorkspaceSubdirs();
8
7
  const DB_PATH = getDatabasePath();
9
- const MEMORY_EMBEDDING_DIM = Number(getConfigValue('MEMORY_EMBEDDING_DIM') ?? '1536') || 1536;
10
8
  const DEFAULT_MEMORY_TOP_K = 5;
11
9
  const MAX_MEMORY_TOP_K = 50;
12
10
  const ALLOW_CROSS_SESSION_MEMORY_FALLBACK = getConfigValue('MEMORY_ALLOW_CROSS_SESSION_FALLBACK')?.trim().toLowerCase() === 'true';
@@ -14,8 +12,6 @@ if (!fs.existsSync(path.dirname(DB_PATH))) {
14
12
  fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
15
13
  }
16
14
  export const db = new Database(DB_PATH);
17
- // Load sqlite-vec C extension
18
- sqliteVec.load(db);
19
15
  db.pragma('foreign_keys = ON');
20
16
  db.exec(`
21
17
  CREATE TABLE IF NOT EXISTS sessions (
@@ -58,11 +54,11 @@ db.exec(`
58
54
  FOREIGN KEY(session_id) REFERENCES sessions(session_id)
59
55
  );
60
56
 
61
- -- Virtual table for vector search
62
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_memory USING vec0(
63
- embedding float[${MEMORY_EMBEDDING_DIM}],
64
- session_id TEXT,
65
- fact_text TEXT
57
+ -- Virtual table for full-text search (FTS5) memory
58
+ CREATE VIRTUAL TABLE IF NOT EXISTS semantic_memory_fts USING fts5(
59
+ session_id UNINDEXED,
60
+ fact_text,
61
+ tokenize="porter unicode61"
66
62
  );
67
63
 
68
64
  CREATE TABLE IF NOT EXISTS reasoning_nodes (
@@ -298,13 +294,6 @@ db.exec(`
298
294
  CREATE INDEX IF NOT EXISTS idx_runtime_budget_events_created
299
295
  ON runtime_budget_events(created_at DESC);
300
296
  `);
301
- function serializeEmbedding(embedding) {
302
- const sqliteVecWithSerializer = sqliteVec;
303
- if (typeof sqliteVecWithSerializer.serializeFloat32 === 'function') {
304
- return sqliteVecWithSerializer.serializeFloat32(embedding);
305
- }
306
- return Buffer.from(new Float32Array(embedding).buffer);
307
- }
308
297
  function normalizeTopK(topK) {
309
298
  if (!Number.isFinite(topK)) {
310
299
  return DEFAULT_MEMORY_TOP_K;
@@ -327,55 +316,26 @@ export function getSessionMessages(sessionId) {
327
316
  const stmt = db.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY rowid ASC');
328
317
  return stmt.all(sessionId);
329
318
  }
330
- export function saveMemoryEmbedding(sessionId, factText, embedding) {
331
- const stmt = db.prepare('INSERT INTO vec_memory (embedding, session_id, fact_text) VALUES (?, ?, ?)');
332
- const result = stmt.run(serializeEmbedding(embedding), sessionId, factText);
319
+ export function saveMemoryFact(sessionId, factText) {
320
+ const stmt = db.prepare('INSERT INTO semantic_memory_fts (session_id, fact_text) VALUES (?, ?)');
321
+ const result = stmt.run(sessionId, factText);
333
322
  return Number(result.lastInsertRowid);
334
323
  }
335
- export function getNearestMemories(queryEmbedding, topK = 5, currentSessionId) {
336
- const boundedTopK = normalizeTopK(topK);
337
- const matcher = serializeEmbedding(queryEmbedding);
338
- const stmt = db.prepare('SELECT rowid AS memory_rowid, session_id, fact_text, distance FROM vec_memory WHERE embedding MATCH ? ORDER BY distance ASC LIMIT ?');
339
- const rows = stmt.all(matcher, boundedTopK * 3);
340
- if (!currentSessionId) {
341
- return rows.slice(0, boundedTopK);
342
- }
343
- const scoped = rows.filter((row) => row.session_id === currentSessionId);
344
- if (!ALLOW_CROSS_SESSION_MEMORY_FALLBACK) {
345
- return scoped.slice(0, boundedTopK);
346
- }
347
- const global = rows.filter((row) => row.session_id !== currentSessionId);
348
- return [...scoped, ...global].slice(0, boundedTopK);
349
- }
350
- /**
351
- * Keyword-based fallback search when vector embeddings are unavailable.
352
- * Uses simple SQL LIKE matching.
353
- */
354
- export function keywordSearchMemories(query, topK = 5, currentSessionId) {
324
+ export function searchMemoriesFTS(query, topK = 5, currentSessionId) {
355
325
  const boundedTopK = normalizeTopK(topK);
356
- // Simple tokenization: remove common punctuation and split by whitespace
357
- const tokens = query
326
+ const words = query
358
327
  .toLowerCase()
359
328
  .replace(/[^\w\s]/g, ' ')
360
329
  .split(/\s+/)
361
- .filter((t) => t.length > 2);
362
- if (tokens.length === 0) {
330
+ .filter((w) => w.length > 2);
331
+ if (words.length === 0) {
363
332
  return [];
364
333
  }
365
- // Build a query with LIKE for each token (OR logic)
366
- // Distance is mocked as 1.0 for keyword matches
367
- const placeholders = tokens.map(() => 'fact_text LIKE ?').join(' OR ');
368
- const sql = `
369
- SELECT rowid AS memory_rowid, session_id, fact_text, 1.0 AS distance
370
- FROM vec_memory
371
- WHERE ${placeholders}
372
- ORDER BY rowid DESC
373
- LIMIT ?
374
- `;
334
+ // Use natural language query format or standard FTS query
335
+ const ftsQuery = words.map((w) => `"${w}"*`).join(' OR ');
336
+ const stmt = db.prepare('SELECT rowid AS memory_rowid, session_id, fact_text, rank AS distance FROM semantic_memory_fts WHERE semantic_memory_fts MATCH ? ORDER BY rank ASC LIMIT ?');
375
337
  try {
376
- const stmt = db.prepare(sql);
377
- const values = tokens.map((t) => `%${t}%`);
378
- const rows = stmt.all(...values, boundedTopK * 3);
338
+ const rows = stmt.all(ftsQuery, boundedTopK * 3);
379
339
  if (!currentSessionId) {
380
340
  return rows.slice(0, boundedTopK);
381
341
  }
@@ -386,10 +346,10 @@ export function keywordSearchMemories(query, topK = 5, currentSessionId) {
386
346
  const global = rows.filter((row) => row.session_id !== currentSessionId);
387
347
  return [...scoped, ...global].slice(0, boundedTopK);
388
348
  }
389
- catch (error) {
390
- const message = error instanceof Error ? error.message : String(error);
391
- console.error(`[DB] keywordSearchMemories failed (tokens=${tokens.length}, topK=${boundedTopK}, scoped=${currentSessionId ? 'yes' : 'no'}): ${message}`);
392
- throw error;
349
+ catch (err) {
350
+ const message = err instanceof Error ? err.message : String(err);
351
+ console.warn(`[DB] FTS search failed for query '${ftsQuery}': ${message}`);
352
+ return [];
393
353
  }
394
354
  }
395
355
  // Re-export domain-specific DB helpers for backwards compatibility.
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { logThought } from '../utils/logger.js';
5
+ import { getWorkspaceDir } from '../config/workspace.js';
5
6
  const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
6
7
  const CODE_LENGTH = 8;
7
8
  const DEFAULT_CODE_TTL_MS = 5 * 60 * 1000; // 5 minutes for device pairing
@@ -33,7 +34,7 @@ export class DevicePairingService {
33
34
  constructor(options = {}) {
34
35
  this.#credentialsDir = options.credentialsDir
35
36
  ? path.resolve(options.credentialsDir)
36
- : path.resolve('memory', 'devices');
37
+ : path.join(getWorkspaceDir(), 'memory', 'devices');
37
38
  this.#codeTtlMs = options.codeTtlMs ?? DEFAULT_CODE_TTL_MS;
38
39
  this.#maxPending = options.maxPendingPerChannel ?? DEFAULT_MAX_PENDING;
39
40
  // Load existing devices
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { getWorkspaceDir } from '../config/workspace.js';
4
5
  const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
5
6
  const CODE_LENGTH = 8;
6
7
  const DEFAULT_CODE_TTL_MS = 60 * 60 * 1000;
@@ -66,7 +67,7 @@ export class DmPairingService {
66
67
  constructor(options = {}) {
67
68
  this.#credentialsDir = options.credentialsDir
68
69
  ? path.resolve(options.credentialsDir)
69
- : path.resolve('memory', 'credentials');
70
+ : path.join(getWorkspaceDir(), 'memory', 'credentials');
70
71
  this.#codeTtlMs =
71
72
  Number.isFinite(options.codeTtlMs) && (options.codeTtlMs ?? 0) > 0
72
73
  ? Number(options.codeTtlMs)
@@ -93,6 +93,18 @@ class HooksService {
93
93
  }
94
94
  return results;
95
95
  }
96
+ async executeHook(event, data = {}) {
97
+ const context = {
98
+ event,
99
+ timestamp: new Date().toISOString(),
100
+ data,
101
+ };
102
+ // Use the sessionId if provided in the data payload
103
+ if (typeof data.sessionId === 'string') {
104
+ context.sessionId = data.sessionId;
105
+ }
106
+ return this.runHook(event, context);
107
+ }
96
108
  async #runScript(scriptPath, context) {
97
109
  const fullPath = path.isAbsolute(scriptPath)
98
110
  ? scriptPath
@@ -2,6 +2,7 @@ import cron from 'node-cron';
2
2
  import { logThought } from '../utils/logger.js';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { getWorkspaceDir } from '../config/workspace.js';
5
6
  /**
6
7
  * Centralized job scheduler for TwinClaw's proactive execution layer.
7
8
  *
@@ -30,7 +31,8 @@ export class JobScheduler {
30
31
  * @param persistenceDir - Directory to store job persistence files
31
32
  */
32
33
  constructor(persistenceDir) {
33
- this.#persistencePath = path.resolve(persistenceDir || './memory', 'scheduler-jobs.json');
34
+ const workspaceMemoryDir = path.join(getWorkspaceDir(), 'memory');
35
+ this.#persistencePath = path.resolve(persistenceDir || workspaceMemoryDir, 'scheduler-jobs.json');
34
36
  this.#loadPersistedJobs();
35
37
  }
36
38
  /** Register a new repeating job. Throws if a job with the same ID already exists. */
@@ -290,6 +290,7 @@ export class ModelRouter {
290
290
  if (lastTriedModelId && lastTriedModelId !== config.id) {
291
291
  this.metrics.failoverCount += 1;
292
292
  this.recordEvent('failover', config, `Automatic fallback ${lastTriedModelId} -> ${config.id}.`);
293
+ console.warn(`[ModelRouter] ⚠️ Fallback triggered: Switching from ${lastTriedModelId} to ${config.id}`);
293
294
  }
294
295
  const payload = {
295
296
  model: config.model,
@@ -317,6 +318,7 @@ export class ModelRouter {
317
318
  const retryWaitMs = Math.min(firstAttempt.rateLimitCooldownMs, this.intelligentPacingMaxWaitMs);
318
319
  if (retryWaitMs > 0) {
319
320
  this.recordEvent('cooldown_wait', config, `Intelligent pacing wait ${retryWaitMs}ms before retrying ${config.id}.`);
321
+ console.warn(`[ModelRouter] ⏳ Rate limited. Intelligent pacing: waiting ${retryWaitMs}ms before retrying ${config.id}...`);
320
322
  await this.sleepFn(retryWaitMs);
321
323
  }
322
324
  const retryCooldown = this.getModelCooldownState(config.id);
@@ -400,6 +402,7 @@ export class ModelRouter {
400
402
  if (lastTriedModelId && lastTriedModelId !== config.id) {
401
403
  this.metrics.failoverCount += 1;
402
404
  this.recordEvent('failover', config, `Automatic fallback ${lastTriedModelId} -> ${config.id}.`);
405
+ console.warn(`[ModelRouter] ⚠️ Streaming Fallback triggered: Switching from ${lastTriedModelId} to ${config.id}`);
403
406
  }
404
407
  const payload = {
405
408
  model: config.model,
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { chunkText, EmbeddingService } from './embedding-service.js';
3
- import { getNearestMemories, keywordSearchMemories, saveMemoryEmbedding, } from './db.js';
2
+ import { chunkText } from './embedding-service.js';
3
+ import { searchMemoriesFTS, saveMemoryFact, } from './db.js';
4
4
  import { getMemoryProvenanceRows, getReasoningEvidenceExpansion, getReasoningNodesByClaimKey, linkMemoryProvenance, upsertReasoningEdge, upsertReasoningNode, } from './db-reasoning.js';
5
- const embeddingService = new EmbeddingService();
5
+ import { getHooksService } from './hooks.js';
6
6
  const NEGATION_PATTERN = /\b(no|not|never|without|cannot|can't|won't|dont|don't|didnt|didn't|isnt|isn't|arent|aren't|wasnt|wasn't|weren't)\b/i;
7
7
  const CLAIM_KEY_STOPWORDS = /\b(a|an|the|and|or|to|of|for|in|on|at|with|is|are|was|were|be|been|being|do|does|did|this|that|it|as|by|from|no|not|never|without|cannot|cant|wont)\b/g;
8
8
  const TASK_HINT_PATTERN = /\b(todo|task|implement|fix|build|create|refactor|ship|deploy)\b/i;
@@ -20,11 +20,7 @@ export async function indexConversationTurn(sessionId, role, content) {
20
20
  const limitedChunks = chunks.slice(0, 2);
21
21
  for (const chunk of limitedChunks) {
22
22
  const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
23
- const embedding = await embeddingService.embedText(taggedChunk);
24
- if (!embedding) {
25
- continue;
26
- }
27
- const memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
23
+ const memoryRowId = saveMemoryFact(sessionId, taggedChunk);
28
24
  const node = buildReasoningNode(sessionId, role, taggedChunk);
29
25
  upsertReasoningNode(node);
30
26
  linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
@@ -41,6 +37,11 @@ export async function indexConversationTurn(sessionId, role, content) {
41
37
  previousNodeId = node.nodeId;
42
38
  reconcileClaimRelations(node);
43
39
  }
40
+ // Trigger proactive context alignment hook
41
+ const hooks = getHooksService();
42
+ hooks.executeHook('afterIndexTurn', { sessionId, role, content }).catch((err) => {
43
+ console.warn(`[SemanticMemory] Hook execution failed for afterIndexTurn:`, err);
44
+ });
44
45
  }
45
46
  catch (err) {
46
47
  const message = err instanceof Error ? err.message : String(err);
@@ -48,24 +49,13 @@ export async function indexConversationTurn(sessionId, role, content) {
48
49
  }
49
50
  }
50
51
  export async function retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK = 5) {
51
- const embedding = await embeddingService.embedText(prompt);
52
52
  const candidateLimit = Math.max(topK * 3, topK);
53
- let nearest;
54
- let fallbackUsed = false;
55
- if (embedding) {
56
- nearest = getNearestMemories(embedding, candidateLimit, sessionId);
57
- }
58
- else {
59
- nearest = keywordSearchMemories(prompt, candidateLimit, sessionId);
60
- fallbackUsed = true;
61
- }
53
+ const nearest = searchMemoriesFTS(prompt, candidateLimit, sessionId);
62
54
  if (nearest.length === 0) {
63
55
  return {
64
56
  context: '',
65
57
  diagnostics: [
66
- fallbackUsed
67
- ? 'Keyword-fallback memory retrieval found no candidates for the active session scope.'
68
- : 'Memory retrieval found no nearest vector candidates for the active session scope.'
58
+ 'Memory retrieval found no FTS candidates for the active session scope.'
69
59
  ],
70
60
  conflictCount: 0,
71
61
  hitCount: 0,
@@ -93,9 +83,7 @@ export async function retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK
93
83
  return {
94
84
  context: `Retrieved evidence-backed memories:\n${lines.join('\n')}${conflictNote}`,
95
85
  diagnostics: [
96
- fallbackUsed
97
- ? `Keyword-fallback memory retrieval ranked ${nearest.length} candidates down to ${ranked.length} items.`
98
- : `Hybrid memory retrieval ranked ${nearest.length} vector candidates down to ${ranked.length} evidence-backed items.`,
86
+ `FTS memory retrieval ranked ${nearest.length} candidates down to ${ranked.length} evidence-backed items.`,
99
87
  `Graph traversal depth=${MAX_GRAPH_TRAVERSAL_DEPTH} collected ${expandedEdges.length} relation edge(s).`,
100
88
  conflictCount > 0
101
89
  ? `Detected contradiction signals in ${conflictCount} candidate(s).`
@@ -207,7 +195,9 @@ function buildGraphRelationCounts(edges) {
207
195
  }
208
196
  function scoreCandidate(sessionId, factText, memoryRowId, distance, options) {
209
197
  const provenance = options.provenance;
210
- const vectorScore = 1 / (1 + Math.max(0, distance));
198
+ // SQLite FTS5 bm25 rank is negative. More negative means better match.
199
+ const ftsRelevance = Math.max(0, -distance);
200
+ const ftsScore = Math.min(1, ftsRelevance * 0.15); // Scale BM25 to 0.0 - 1.0 range
211
201
  const graphCounts = provenance
212
202
  ? options.graphRelationCounts.get(provenance.nodeId) ?? {
213
203
  supports: 0,
@@ -223,7 +213,7 @@ function scoreCandidate(sessionId, factText, memoryRowId, distance, options) {
223
213
  const relationScore = Math.min(1, supports * 0.2 + depends * 0.11 + derived * 0.08);
224
214
  const recencyScore = estimateRecencyScore(provenance?.updatedAt);
225
215
  const contradictionPenalty = Math.min(0.35, contradicts * 0.08);
226
- const score = vectorScore * 0.62 + relationScore * 0.26 + recencyScore * 0.12 - contradictionPenalty;
216
+ const score = ftsScore * 0.62 + relationScore * 0.26 + recencyScore * 0.12 - contradictionPenalty;
227
217
  return {
228
218
  sessionId,
229
219
  factText,
@@ -5,6 +5,7 @@ import { randomUUID } from 'node:crypto';
5
5
  import { executeProgram, executeShell } from './shell.js';
6
6
  import { logToolCall } from '../utils/logger.js';
7
7
  import { validatePathInWorkspace } from '../config/workspace.js';
8
+ import { getPersonaEditorTool } from '../tools/persona-editor.js';
8
9
  function buildReadFileSkill() {
9
10
  return {
10
11
  name: 'fs.read',
@@ -1038,6 +1039,39 @@ function buildVenvSkill() {
1038
1039
  },
1039
1040
  };
1040
1041
  }
1042
+ function buildPersonaEditorSkill() {
1043
+ return {
1044
+ name: 'persona.edit',
1045
+ group: 'group:core',
1046
+ aliases: ['edit_persona', 'update_user_profile', 'update_soul'],
1047
+ description: 'Programmatically edit the agent\'s identity (soul.md) or user profile (user.md).',
1048
+ parameters: {
1049
+ type: 'object',
1050
+ properties: {
1051
+ target: { type: 'string', enum: ['user', 'soul'] },
1052
+ operation: { type: 'string', enum: ['append', 'replace', 'clear'] },
1053
+ content: { type: 'string' },
1054
+ },
1055
+ required: ['target', 'operation'],
1056
+ },
1057
+ async execute(input) {
1058
+ if (input.operation !== 'clear' && typeof input.content !== 'string') {
1059
+ return { ok: false, output: 'Content must be provided for append/replace operations' };
1060
+ }
1061
+ const request = input;
1062
+ const editor = getPersonaEditorTool();
1063
+ const result = await editor.editPersona({
1064
+ target: request.target,
1065
+ operation: request.operation,
1066
+ content: request.content || '',
1067
+ });
1068
+ return {
1069
+ ok: result.success,
1070
+ output: result.message,
1071
+ };
1072
+ },
1073
+ };
1074
+ }
1041
1075
  export function createBuiltinSkills() {
1042
1076
  return [
1043
1077
  buildReadFileSkill(),
@@ -1073,5 +1107,6 @@ export function createBuiltinSkills() {
1073
1107
  buildDefenderSkill(),
1074
1108
  buildPowerShellISESkill(),
1075
1109
  buildVenvSkill(),
1110
+ buildPersonaEditorSkill(),
1076
1111
  ];
1077
1112
  }
@@ -0,0 +1,56 @@
1
+ import { ensureIdentityFiles } from '../config/identity-bootstrap.js';
2
+ import { getWorkspaceDir } from '../config/workspace.js';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs/promises';
5
+ import { logThought } from '../utils/logger.js';
6
+ export class PersonaEditorTool {
7
+ async editPersona(request) {
8
+ try {
9
+ const workspaceDir = getWorkspaceDir();
10
+ const identityDir = path.join(workspaceDir, 'identity');
11
+ // Ensure the directory and foundation files exist
12
+ ensureIdentityFiles();
13
+ const targetFile = request.target === 'user' ? 'user.md' : 'soul.md';
14
+ const filePath = path.join(identityDir, targetFile);
15
+ let currentContent = '';
16
+ try {
17
+ currentContent = await fs.readFile(filePath, 'utf-8');
18
+ }
19
+ catch (err) {
20
+ // If it doesn't exist, we treat it as empty
21
+ currentContent = '';
22
+ }
23
+ let newContent = currentContent;
24
+ switch (request.operation) {
25
+ case 'append':
26
+ // Append with an explicit timestamp so the agent knows when this was learned
27
+ const timestamp = new Date().toISOString();
28
+ newContent = `${currentContent.trim()}\n\n[Learned at ${timestamp}]\n${request.content.trim()}`;
29
+ break;
30
+ case 'replace':
31
+ newContent = request.content.trim();
32
+ break;
33
+ case 'clear':
34
+ newContent = '';
35
+ break;
36
+ default:
37
+ return { success: false, message: `Unknown operation: ${request.operation}` };
38
+ }
39
+ await fs.writeFile(filePath, newContent, 'utf-8');
40
+ await logThought(`[PersonaEditor] Updated ${targetFile} via operation '${request.operation}'. Let the system know.`);
41
+ return { success: true, message: `Successfully updated ${targetFile}` };
42
+ }
43
+ catch (err) {
44
+ const errorMsg = err instanceof Error ? err.message : String(err);
45
+ await logThought(`[PersonaEditor] Error updating persona: ${errorMsg}`);
46
+ return { success: false, message: `Failed to edit persona: ${errorMsg}` };
47
+ }
48
+ }
49
+ }
50
+ let personaEditorTool = null;
51
+ export function getPersonaEditorTool() {
52
+ if (!personaEditorTool) {
53
+ personaEditorTool = new PersonaEditorTool();
54
+ }
55
+ return personaEditorTool;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -53,7 +53,6 @@
53
53
  "playwright": "^1.58.2",
54
54
  "playwright-core": "^1.58.2",
55
55
  "qrcode-terminal": "^0.12.0",
56
- "sqlite-vec": "^0.1.7-alpha.2",
57
56
  "ts-node": "^10.9.2",
58
57
  "tsx": "^4.21.0",
59
58
  "typescript": "^5.9.3",
@@ -83,4 +82,4 @@
83
82
  "request": "npm:@cypress/request@^3.0.10",
84
83
  "request-promise": "npm:@cypress/request-promise@^5.0.0"
85
84
  }
86
- }
85
+ }