@aeriondyseti/vector-memory-mcp 2.2.2 → 2.2.6

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 (31) hide show
  1. package/README.md +33 -13
  2. package/package.json +8 -7
  3. package/scripts/lancedb-extract.ts +111 -46
  4. package/scripts/migrate-from-lancedb.ts +2 -2
  5. package/scripts/smoke-test.ts +1 -1
  6. package/scripts/sync-version.ts +35 -0
  7. package/scripts/warmup.ts +2 -2
  8. package/{src → server}/config/index.ts +10 -2
  9. package/{src/db → server/core}/connection.ts +10 -2
  10. package/{src/db → server/core}/conversation.repository.ts +1 -1
  11. package/{src/services → server/core}/conversation.service.ts +2 -2
  12. package/{src/db → server/core}/memory.repository.ts +5 -1
  13. package/{src/services → server/core}/memory.service.ts +20 -4
  14. package/server/core/migration.service.ts +882 -0
  15. package/server/core/migrations.ts +115 -0
  16. package/{src/services → server/core}/parsers/claude-code.parser.ts +1 -1
  17. package/{src/services → server/core}/parsers/types.ts +1 -1
  18. package/{src → server}/index.ts +13 -10
  19. package/{src → server}/migration.ts +2 -2
  20. package/{src → server/transports}/http/mcp-transport.ts +2 -2
  21. package/{src → server/transports}/http/server.ts +34 -4
  22. package/{src → server/transports}/mcp/handlers.ts +5 -5
  23. package/server/transports/mcp/resources.ts +161 -0
  24. package/{src → server/transports}/mcp/server.ts +14 -3
  25. package/server/utils/formatting.ts +143 -0
  26. package/src/db/migrations.ts +0 -108
  27. /package/{src/types → server/core}/conversation.ts +0 -0
  28. /package/{src/services → server/core}/embeddings.service.ts +0 -0
  29. /package/{src/types → server/core}/memory.ts +0 -0
  30. /package/{src/db → server/core}/sqlite-utils.ts +0 -0
  31. /package/{src → server/transports}/mcp/tools.ts +0 -0
@@ -0,0 +1,115 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ /**
4
+ * Pre-migration step: remove vec0 virtual table entries from sqlite_master
5
+ * and drop their shadow tables using the sqlite3 CLI.
6
+ *
7
+ * Must run BEFORE bun:sqlite opens the database because:
8
+ * - bun:sqlite cannot modify sqlite_master (no writable_schema support)
9
+ * - DROP TABLE on a virtual table requires the extension module to be loaded
10
+ * - SQLite 3.51+ has defensive mode on by default, requiring .dbconfig override
11
+ *
12
+ * Safe to call on any database — it's a no-op if there are no vec0 tables.
13
+ */
14
+ export function removeVec0Tables(dbPath: string): void {
15
+ const result = Bun.spawnSync({
16
+ cmd: ["sqlite3", dbPath],
17
+ stdin: new TextEncoder().encode(
18
+ [
19
+ ".dbconfig defensive off",
20
+ ".dbconfig writable_schema on",
21
+ // Drop shadow tables (regular tables, no extension needed)
22
+ "DROP TABLE IF EXISTS memories_vec_rowids;",
23
+ "DROP TABLE IF EXISTS memories_vec_chunks;",
24
+ "DROP TABLE IF EXISTS memories_vec_info;",
25
+ "DROP TABLE IF EXISTS memories_vec_vector_chunks00;",
26
+ "DROP TABLE IF EXISTS memories_vec_migration_tmp;",
27
+ "DROP TABLE IF EXISTS conversation_history_vec_rowids;",
28
+ "DROP TABLE IF EXISTS conversation_history_vec_chunks;",
29
+ "DROP TABLE IF EXISTS conversation_history_vec_info;",
30
+ "DROP TABLE IF EXISTS conversation_history_vec_vector_chunks00;",
31
+ "DROP TABLE IF EXISTS conversation_history_vec_migration_tmp;",
32
+ // Remove orphaned vec0 virtual table entries from schema
33
+ "DELETE FROM sqlite_master WHERE sql LIKE '%vec0%';",
34
+ ].join("\n"),
35
+ ),
36
+ });
37
+ if (result.exitCode !== 0) {
38
+ const stderr = result.stderr.toString().trim();
39
+ if (!stderr.includes("unable to open database")) {
40
+ throw new Error(`vec0 cleanup failed: ${stderr}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Run all schema migrations. Safe to call on every startup (uses IF NOT EXISTS).
47
+ *
48
+ * IMPORTANT: Call removeVec0Tables(dbPath) before opening the database
49
+ * with bun:sqlite if the database may contain vec0 virtual tables.
50
+ */
51
+ export function runMigrations(db: Database): void {
52
+ // -- Memories --
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS memories (
55
+ id TEXT PRIMARY KEY,
56
+ content TEXT NOT NULL,
57
+ metadata TEXT NOT NULL DEFAULT '{}',
58
+ created_at INTEGER NOT NULL,
59
+ updated_at INTEGER NOT NULL,
60
+ superseded_by TEXT,
61
+ usefulness REAL NOT NULL DEFAULT 0.0,
62
+ access_count INTEGER NOT NULL DEFAULT 0,
63
+ last_accessed INTEGER
64
+ )
65
+ `);
66
+
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS memories_vec (
69
+ id TEXT PRIMARY KEY,
70
+ vector BLOB NOT NULL
71
+ )
72
+ `);
73
+
74
+ db.exec(`
75
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
76
+ id UNINDEXED,
77
+ content
78
+ )
79
+ `);
80
+
81
+ // -- Conversation History --
82
+ db.exec(`
83
+ CREATE TABLE IF NOT EXISTS conversation_history (
84
+ id TEXT PRIMARY KEY,
85
+ content TEXT NOT NULL,
86
+ metadata TEXT NOT NULL DEFAULT '{}',
87
+ created_at INTEGER NOT NULL,
88
+ session_id TEXT NOT NULL,
89
+ role TEXT NOT NULL,
90
+ message_index_start INTEGER NOT NULL,
91
+ message_index_end INTEGER NOT NULL,
92
+ project TEXT NOT NULL
93
+ )
94
+ `);
95
+
96
+ db.exec(`
97
+ CREATE TABLE IF NOT EXISTS conversation_history_vec (
98
+ id TEXT PRIMARY KEY,
99
+ vector BLOB NOT NULL
100
+ )
101
+ `);
102
+
103
+ db.exec(`
104
+ CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
105
+ id UNINDEXED,
106
+ content
107
+ )
108
+ `);
109
+
110
+ // -- Indexes --
111
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_session_id ON conversation_history(session_id)`);
112
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_project ON conversation_history(project)`);
113
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_role ON conversation_history(role)`);
114
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_created_at ON conversation_history(created_at)`);
115
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFile, readdir, stat } from "fs/promises";
2
2
  import { basename, dirname, join } from "path";
3
- import type { ParsedMessage, SessionFileInfo } from "../../types/conversation.js";
3
+ import type { ParsedMessage, SessionFileInfo } from "../conversation.js";
4
4
  import type { SessionLogParser } from "./types.js";
5
5
 
6
6
  // UUID pattern for session IDs
@@ -1,4 +1,4 @@
1
- import type { ParsedMessage, SessionFileInfo } from "../../types/conversation.js";
1
+ import type { ParsedMessage, SessionFileInfo } from "../conversation.js";
2
2
 
3
3
  /** Interface for parsing session log files into structured messages */
4
4
  export interface SessionLogParser {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { loadConfig, parseCliArgs } from "./config/index.js";
4
- import { connectToDatabase } from "./db/connection.js";
5
- import { MemoryRepository } from "./db/memory.repository.js";
6
- import { ConversationRepository } from "./db/conversation.repository.js";
7
- import { EmbeddingsService } from "./services/embeddings.service.js";
8
- import { MemoryService } from "./services/memory.service.js";
9
- import { ConversationHistoryService } from "./services/conversation.service.js";
10
- import { startServer } from "./mcp/server.js";
11
- import { startHttpServer, removeLockfile } from "./http/server.js";
4
+ import { connectToDatabase } from "./core/connection.js";
5
+ import { MemoryRepository } from "./core/memory.repository.js";
6
+ import { ConversationRepository } from "./core/conversation.repository.js";
7
+ import { EmbeddingsService } from "./core/embeddings.service.js";
8
+ import { MemoryService } from "./core/memory.service.js";
9
+ import { ConversationHistoryService } from "./core/conversation.service.js";
10
+ import { startServer } from "./transports/mcp/server.js";
11
+ import { startHttpServer } from "./transports/http/server.js";
12
12
  import { isLanceDbDirectory, migrate, formatMigrationSummary } from "./migration.js";
13
13
 
14
14
  async function runMigrate(args: string[]): Promise<void> {
@@ -56,7 +56,7 @@ async function main(): Promise<void> {
56
56
  `[vector-memory-mcp] ⚠️ Legacy LanceDB data detected at ${config.dbPath}\n` +
57
57
  ` Your data must be migrated to the new SQLite format.\n` +
58
58
  ` Run: vector-memory-mcp migrate\n` +
59
- ` Or: bun run src/index.ts migrate\n`
59
+ ` Or: bun run server/index.ts migrate\n`
60
60
  );
61
61
  process.exit(1);
62
62
  }
@@ -69,6 +69,10 @@ async function main(): Promise<void> {
69
69
  const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
70
70
  const memoryService = new MemoryService(repository, embeddings);
71
71
 
72
+ if (config.pluginMode) {
73
+ console.error("[vector-memory-mcp] Running in plugin mode");
74
+ }
75
+
72
76
  // Conditionally initialize conversation history indexing
73
77
  if (config.conversationHistory.enabled) {
74
78
  const conversationRepository = new ConversationRepository(db);
@@ -88,7 +92,6 @@ async function main(): Promise<void> {
88
92
  // Graceful shutdown handler
89
93
  const shutdown = () => {
90
94
  console.error("[vector-memory-mcp] Shutting down...");
91
- removeLockfile();
92
95
  if (httpStop) httpStop();
93
96
  db.close();
94
97
  process.exit(0);
@@ -12,8 +12,8 @@
12
12
  import { existsSync, statSync } from "fs";
13
13
  import { resolve, dirname } from "path";
14
14
  import { fileURLToPath } from "url";
15
- import { connectToDatabase } from "./db/connection.js";
16
- import { serializeVector } from "./db/sqlite-utils.js";
15
+ import { connectToDatabase } from "./core/connection.js";
16
+ import { serializeVector } from "./core/sqlite-utils.js";
17
17
 
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
 
@@ -24,8 +24,8 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
24
24
  import { tools } from "../mcp/tools.js";
25
25
  import { handleToolCall } from "../mcp/handlers.js";
26
26
  import { SERVER_INSTRUCTIONS } from "../mcp/server.js";
27
- import { VERSION } from "../config/index.js";
28
- import type { MemoryService } from "../services/memory.service.js";
27
+ import { VERSION } from "../../config/index.js";
28
+ import type { MemoryService } from "../../core/memory.service.js";
29
29
 
30
30
  interface Session {
31
31
  server: Server;
@@ -3,11 +3,12 @@ import { cors } from "hono/cors";
3
3
  import { createServer } from "net";
4
4
  import { writeFileSync, mkdirSync, unlinkSync } from "fs";
5
5
  import { join } from "path";
6
- import type { MemoryService } from "../services/memory.service.js";
7
- import type { Config } from "../config/index.js";
8
- import { isDeleted } from "../types/memory.js";
6
+ import type { MemoryService } from "../../core/memory.service.js";
7
+ import type { Config } from "../../config/index.js";
8
+ import { isDeleted } from "../../core/memory.js";
9
9
  import { createMcpRoutes } from "./mcp-transport.js";
10
- import type { Memory, SearchIntent } from "../types/memory.js";
10
+ import type { Memory, SearchIntent } from "../../core/memory.js";
11
+ import { MigrationService } from "../../core/migration.service.js";
11
12
 
12
13
  /**
13
14
  * Check if a port is available by attempting to bind to it
@@ -109,6 +110,7 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
109
110
  embeddingModel: config.embeddingModel,
110
111
  embeddingDimension: config.embeddingDimension,
111
112
  historyEnabled: config.conversationHistory.enabled,
113
+ pluginMode: config.pluginMode,
112
114
  },
113
115
  });
114
116
  });
@@ -243,6 +245,34 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
243
245
  }
244
246
  });
245
247
 
248
+ // Migrate from external memory database
249
+ app.post("/migrate", async (c) => {
250
+ try {
251
+ const body = await c.req.json().catch(() => null);
252
+ if (!body || typeof body !== "object") {
253
+ return c.json({ error: "Invalid or missing JSON body" }, 400);
254
+ }
255
+ const source = body.source;
256
+
257
+ if (!source || typeof source !== "string") {
258
+ return c.json({ error: "Missing or invalid 'source' field" }, 400);
259
+ }
260
+
261
+ const repository = memoryService.getRepository();
262
+ const migrationService = new MigrationService(
263
+ repository,
264
+ memoryService.getEmbeddings(),
265
+ repository.getDb(),
266
+ );
267
+
268
+ const result = await migrationService.migrate(source);
269
+ return c.json(result);
270
+ } catch (error) {
271
+ const message = error instanceof Error ? error.message : "Unknown error";
272
+ return c.json({ error: message }, 500);
273
+ }
274
+ });
275
+
246
276
  // Get single memory
247
277
  app.get("/memories/:id", async (c) => {
248
278
  try {
@@ -1,9 +1,9 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
- import type { MemoryService } from "../services/memory.service.js";
3
- import type { ConversationHistoryService } from "../services/conversation.service.js";
4
- import type { SearchIntent } from "../types/memory.js";
5
- import type { HistoryFilters, SearchResult } from "../types/conversation.js";
6
- import { DEBUG } from "../config/index.js";
2
+ import type { MemoryService } from "../../core/memory.service.js";
3
+ import type { ConversationHistoryService } from "../../core/conversation.service.js";
4
+ import type { SearchIntent } from "../../core/memory.js";
5
+ import type { HistoryFilters, SearchResult } from "../../core/conversation.js";
6
+ import { DEBUG } from "../../config/index.js";
7
7
 
8
8
  /**
9
9
  * Safely coerce a tool argument to an array. Handles the case where the MCP
@@ -0,0 +1,161 @@
1
+ const MIGRATE_GUIDE = `# Migrating External Memory Databases
2
+
3
+ The vector-memory-mcp server exposes a \`POST /migrate\` HTTP endpoint that imports
4
+ memories from other database formats into the running instance. All imported
5
+ content is re-embedded with the server's current embedding model to guarantee
6
+ consistency.
7
+
8
+ ## Endpoint
9
+
10
+ \`\`\`
11
+ POST http://<host>:<port>/migrate
12
+ Content-Type: application/json
13
+
14
+ { "source": "/absolute/path/to/source/database" }
15
+ \`\`\`
16
+
17
+ ## Discovering the Server Port
18
+
19
+ The HTTP server writes a lockfile at \`.vector-memory/server.lock\` in the
20
+ project's working directory. Read it to discover the current port:
21
+
22
+ \`\`\`json
23
+ { "port": 3271, "pid": 12345 }
24
+ \`\`\`
25
+
26
+ ## Supported Source Formats
27
+
28
+ The endpoint auto-detects the source format from the path provided.
29
+
30
+ ### 1. LanceDB Directory
31
+ Provide the path to a LanceDB data directory (contains \`.lance\` files or
32
+ \`_versions\`/\`_indices\` subdirectories). Both memories and conversation
33
+ history are imported.
34
+
35
+ \`\`\`json
36
+ { "source": "/path/to/project/.vector-memory" }
37
+ \`\`\`
38
+
39
+ ### 2. Own SQLite (Current or Older Schema)
40
+ Provide the path to a \`.db\` file that was created by any version of
41
+ vector-memory-mcp. The migrator handles missing columns (e.g. \`usefulness\`,
42
+ \`access_count\`) by using sensible defaults. Both memories and conversation
43
+ history are imported.
44
+
45
+ \`\`\`json
46
+ { "source": "/path/to/old-project/.vector-memory/memories.db" }
47
+ \`\`\`
48
+
49
+ ### 3. CCCMemory SQLite
50
+ Provide the path to a CCCMemory database. The migrator extracts from the
51
+ \`decisions\`, \`mistakes\`, \`methodologies\`, \`research_findings\`,
52
+ \`solution_patterns\`, and \`working_memory\` tables. Each record is tagged
53
+ with \`source_type: "cccmemory"\` and the appropriate \`memory_type\` in
54
+ metadata.
55
+
56
+ \`\`\`json
57
+ { "source": "/path/to/cccmemory.db" }
58
+ \`\`\`
59
+
60
+ ### 4. MCP Memory Service SQLite
61
+ Provide the path to an mcp-memory-service database. Memories with
62
+ \`deleted_at IS NULL\` are imported. Tags and memory type are preserved in
63
+ metadata.
64
+
65
+ \`\`\`json
66
+ { "source": "/path/to/mcp-memory-service.db" }
67
+ \`\`\`
68
+
69
+ ### 5. MIF JSON (Shodh Memory Interchange Format)
70
+ Provide the path to a \`.json\` file exported from Shodh Memory. The file must
71
+ contain a top-level \`memories\` array. Memory type, tags, entities, and source
72
+ metadata are preserved.
73
+
74
+ \`\`\`json
75
+ { "source": "/path/to/export.mif.json" }
76
+ \`\`\`
77
+
78
+ ## Response
79
+
80
+ The endpoint returns a JSON summary upon completion:
81
+
82
+ \`\`\`json
83
+ {
84
+ "source": "/path/to/source",
85
+ "format": "own-sqlite",
86
+ "memoriesImported": 142,
87
+ "memoriesSkipped": 3,
88
+ "conversationsImported": 0,
89
+ "conversationsSkipped": 0,
90
+ "errors": [],
91
+ "durationMs": 8320
92
+ }
93
+ \`\`\`
94
+
95
+ - **memoriesImported**: Number of new memories written to the database.
96
+ - **memoriesSkipped**: Records skipped because a memory with the same ID
97
+ already exists (safe for idempotent re-runs).
98
+ - **conversationsImported / conversationsSkipped**: Same, for conversation
99
+ history chunks (LanceDB and own-sqlite formats only).
100
+ - **errors**: Per-record errors that did not abort the migration.
101
+ - **durationMs**: Wall-clock time for the entire operation.
102
+
103
+ ## Important Notes
104
+
105
+ - **Re-embedding**: All content is re-embedded regardless of the source format.
106
+ This ensures vector consistency with the server's current model but means the
107
+ operation can take time for large databases (~50ms per record).
108
+ - **Idempotent**: Running the same migration twice is safe. Duplicate IDs are
109
+ skipped.
110
+ - **Non-destructive**: The source database is opened read-only and is never
111
+ modified.
112
+ - **Batched writes**: Records are inserted in batches of 100 within
113
+ transactions. If the process is interrupted, already-committed batches are
114
+ durable.
115
+ - **Error isolation**: A single bad record does not abort the migration. Check
116
+ the \`errors\` array in the response for any per-record failures.
117
+
118
+ ## Workflow Example
119
+
120
+ 1. Locate the source database file or directory.
121
+ 2. Read \`.vector-memory/server.lock\` to get the port.
122
+ 3. Send the migrate request:
123
+ \`\`\`bash
124
+ curl -X POST http://127.0.0.1:3271/migrate \\
125
+ -H "Content-Type: application/json" \\
126
+ -d '{"source": "/path/to/old/memories.db"}'
127
+ \`\`\`
128
+ 4. Inspect the response summary.
129
+ 5. Verify imported memories with a search:
130
+ \`\`\`bash
131
+ curl -X POST http://127.0.0.1:3271/search \\
132
+ -H "Content-Type: application/json" \\
133
+ -d '{"query": "test query", "limit": 5}'
134
+ \`\`\`
135
+ `;
136
+
137
+ export const resources = [
138
+ {
139
+ uri: "vector-memory://guides/migrate",
140
+ name: "Migration Guide",
141
+ description:
142
+ "How to use the POST /migrate HTTP endpoint to import memories from external database formats (LanceDB, older SQLite, CCCMemory, MCP Memory Service, MIF JSON) into the running vector-memory instance.",
143
+ mimeType: "text/markdown",
144
+ },
145
+ ];
146
+
147
+ const RESOURCE_CONTENT: Record<string, string> = {
148
+ "vector-memory://guides/migrate": MIGRATE_GUIDE,
149
+ };
150
+
151
+ export function readResource(uri: string): {
152
+ contents: Array<{ uri: string; mimeType: string; text: string }>;
153
+ } {
154
+ const text = RESOURCE_CONTENT[uri];
155
+ if (!text) {
156
+ throw new Error(`Resource not found: ${uri}`);
157
+ }
158
+ return {
159
+ contents: [{ uri, mimeType: "text/markdown", text }],
160
+ };
161
+ }
@@ -3,12 +3,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import {
4
4
  CallToolRequestSchema,
5
5
  ListToolsRequestSchema,
6
+ ListResourcesRequestSchema,
7
+ ReadResourceRequestSchema,
6
8
  } from "@modelcontextprotocol/sdk/types.js";
9
+ import { resources, readResource } from "./resources.js";
7
10
 
8
11
  import { tools } from "./tools.js";
9
12
  import { handleToolCall } from "./handlers.js";
10
- import type { MemoryService } from "../services/memory.service.js";
11
- import { VERSION } from "../config/index.js";
13
+ import type { MemoryService } from "../../core/memory.service.js";
14
+ import { VERSION } from "../../config/index.js";
12
15
 
13
16
  export const SERVER_INSTRUCTIONS = `This server is the user's canonical memory system. It provides persistent, semantic vector memory that survives across conversations and sessions.
14
17
 
@@ -20,7 +23,7 @@ export function createServer(memoryService: MemoryService): Server {
20
23
  const server = new Server(
21
24
  { name: "vector-memory-mcp", version: VERSION },
22
25
  {
23
- capabilities: { tools: {} },
26
+ capabilities: { tools: {}, resources: {} },
24
27
  instructions: SERVER_INSTRUCTIONS,
25
28
  }
26
29
  );
@@ -34,6 +37,14 @@ export function createServer(memoryService: MemoryService): Server {
34
37
  return handleToolCall(name, args, memoryService);
35
38
  });
36
39
 
40
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
41
+ return { resources };
42
+ });
43
+
44
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
45
+ return readResource(request.params.uri);
46
+ });
47
+
37
48
  return server;
38
49
  }
39
50
 
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Shared text formatting utilities.
3
+ *
4
+ * Provides ANSI styling, Nerd Font icons, horizontal rules, structured
5
+ * message builders, debug logging, and time formatting. Used by both the
6
+ * MCP server and the Claude Code plugin hooks.
7
+ */
8
+
9
+ // ── ANSI escape codes ───────────────────────────────────────────────
10
+
11
+ export const ansi = {
12
+ reset: "\x1b[0m",
13
+ bold: "\x1b[1m",
14
+ dim: "\x1b[2m",
15
+ italic: "\x1b[3m",
16
+ underline: "\x1b[4m",
17
+
18
+ // Foreground colors
19
+ red: "\x1b[31m",
20
+ green: "\x1b[32m",
21
+ yellow: "\x1b[33m",
22
+ blue: "\x1b[34m",
23
+ magenta: "\x1b[35m",
24
+ cyan: "\x1b[36m",
25
+ white: "\x1b[37m",
26
+ gray: "\x1b[90m",
27
+ } as const;
28
+
29
+ // ── Nerd Font glyphs (single-width) ────────────────────────────────
30
+
31
+ export const icon = {
32
+ check: "\uf00c", // nf-fa-check
33
+ cross: "\uf00d", // nf-fa-close
34
+ book: "\uf02d", // nf-fa-book
35
+ branch: "\ue0a0", // Powerline branch
36
+ clock: "\uf017", // nf-fa-clock_o
37
+ warning: "\uf071", // nf-fa-warning
38
+ bolt: "\uf0e7", // nf-fa-bolt
39
+ brain: "\uf5dc", // nf-mdi-brain
40
+ search: "\uf002", // nf-fa-search
41
+ gear: "\uf013", // nf-fa-gear
42
+ database: "\uf1c0", // nf-fa-database
43
+ arrow: "\uf061", // nf-fa-arrow_right
44
+ dot: "\u00b7", // middle dot (standard unicode)
45
+ } as const;
46
+
47
+ // ── Rule line ───────────────────────────────────────────────────────
48
+
49
+ const RULE_WIDTH = 42;
50
+
51
+ /**
52
+ * Create a horizontal rule with an optional inline title.
53
+ * e.g. "── Vector Memory ──────────────────────"
54
+ */
55
+ export function rule(title?: string): string {
56
+ if (!title) {
57
+ return `${ansi.cyan}${"─".repeat(RULE_WIDTH)}${ansi.reset}`;
58
+ }
59
+ const label = ` ${ansi.bold}${title}${ansi.reset} `;
60
+ // "── " prefix = 3 visual chars
61
+ const prefix = `${ansi.cyan}── ${ansi.reset}`;
62
+ // Calculate remaining dashes (account for title visual length)
63
+ const remaining = RULE_WIDTH - 3 - title.length - 2; // 2 for spaces around title
64
+ const suffix = `${ansi.cyan}${"─".repeat(Math.max(1, remaining))}${ansi.reset}`;
65
+ return `${prefix}${label}${suffix}`;
66
+ }
67
+
68
+ // ── System message builder ──────────────────────────────────────────
69
+
70
+ export interface MessageLine {
71
+ icon?: string;
72
+ iconColor?: string;
73
+ text: string;
74
+ }
75
+
76
+ /**
77
+ * Build a user-facing systemMessage with horizontal rules and content lines.
78
+ *
79
+ * Output format:
80
+ * ── Title ──────────────────────────────
81
+ * icon text
82
+ * icon text
83
+ * ──────────────────────────────────────
84
+ *
85
+ * Prepends an empty line so the content starts below the hook label prefix.
86
+ */
87
+ export function buildSystemMessage(
88
+ title: string,
89
+ lines: MessageLine[]
90
+ ): string {
91
+ const parts = [
92
+ "", // push below "HookName says:" prefix
93
+ rule(title),
94
+ ];
95
+
96
+ for (const line of lines) {
97
+ if (line.icon) {
98
+ const color = line.iconColor ?? "";
99
+ const reset = line.iconColor ? ansi.reset : "";
100
+ parts.push(` ${color}${line.icon}${reset} ${line.text}`);
101
+ } else {
102
+ parts.push(` ${line.text}`);
103
+ }
104
+ }
105
+
106
+ parts.push(rule());
107
+ return parts.join("\n");
108
+ }
109
+
110
+ // ── Diagnostic logging ──────────────────────────────────────────────
111
+
112
+ /**
113
+ * Log a diagnostic message to stderr when VECTOR_MEMORY_DEBUG=1.
114
+ */
115
+ export function debug(label: string, message: string): void {
116
+ if (process.env.VECTOR_MEMORY_DEBUG !== "1") return;
117
+ console.error(
118
+ `${ansi.gray}[${label}]${ansi.reset} ${ansi.dim}${message}${ansi.reset}`
119
+ );
120
+ }
121
+
122
+ // ── Time formatting ─────────────────────────────────────────────────
123
+
124
+ export function timeAgo(iso: string): string {
125
+ const now = Date.now();
126
+ const then = new Date(iso).getTime();
127
+ if (Number.isNaN(then)) {
128
+ debug("timeAgo", `invalid ISO string: ${iso}`);
129
+ return "unknown";
130
+ }
131
+ const seconds = Math.floor((now - then) / 1000);
132
+ if (seconds < 0) {
133
+ debug("timeAgo", `negative delta (${seconds}s) — clock skew or future timestamp`);
134
+ return "just now";
135
+ }
136
+ if (seconds < 60) return `${seconds}s ago`;
137
+ const minutes = Math.floor(seconds / 60);
138
+ if (minutes < 60) return `${minutes}m ago`;
139
+ const hours = Math.floor(minutes / 60);
140
+ if (hours < 24) return `${hours}h ago`;
141
+ const days = Math.floor(hours / 24);
142
+ return `${days}d ago`;
143
+ }